From 129a84684f5a4d2af079bd60588a7b0898db4e54 Mon Sep 17 00:00:00 2001 From: Charles Hoskinson Date: Wed, 10 Jun 2026 13:49:06 -0600 Subject: [PATCH 01/15] ci: wire TSan suppressions, retire MSan leg, fix dependency scan Suppression file at root, issue-linked (#38, #39); llvm-symbolizer installed so suppressions actually match; tsan presets carry the same TSAN_OPTIONS. Wrong-thread tests skip under TSan instead of being suppressed. MSan matrix leg removed (re-entry tracked in #40). osv-scanner invocation replaced with a recursive JSON scan; mutes stay until a triaged dispatch run lands. Gate demotion rule recorded in CONTRIBUTING. TSan allow_failure stays until the first suppressed run is green (#38). --- .github/workflows/ci.yml | 44 +++++++++++++------------------ CMakePresets.json | 4 +-- CONTRIBUTING.md | 25 +++++++++++++++--- tests/unit/test_thread_safety.cpp | 17 ++++++++++++ tsan-suppressions.txt | 21 +++++++++++++++ 5 files changed, 81 insertions(+), 30 deletions(-) create mode 100644 tsan-suppressions.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49eb0b2..4185c64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -337,7 +337,7 @@ jobs: github.event_name == 'workflow_dispatch' strategy: matrix: - sanitizer: [address, undefined, thread, memory] + sanitizer: [address, undefined, thread] include: # Note: -stdlib=libc++ is CXX-only; c_flags omit it to avoid -Wunused-command-line-argument - sanitizer: address @@ -348,29 +348,20 @@ jobs: flags: "-stdlib=libc++ -fsanitize=undefined -fno-omit-frame-pointer" c_flags: "-fsanitize=undefined -fno-omit-frame-pointer" env: "UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1" - # TSan detects pre-existing data races in engine global state - # (g_active_instance), CrashBreadcrumb::add(), and cross-thread - # tests that intentionally exercise wrong-thread paths. - # Mark allow-failure until REQ-TH-004 mixer/global state fixes land. - # Exit plan (2026-06-10): Sprint 7 tracks removing allow_failure - # after the remaining engine global-state races are fixed. + # Known races are suppressed via the issue-linked tsan-suppressions.txt + # at repo root (one entry per root cause; see issues #38, #39). + # Intentional wrong-thread tests skip under TSan in code. + # allow_failure is temporary for the suppression bring-up and is + # removed once the first suppressed run is green (issue #38). - sanitizer: thread flags: "-stdlib=libc++ -fsanitize=thread -fno-omit-frame-pointer" c_flags: "-fsanitize=thread -fno-omit-frame-pointer" - env: "TSAN_OPTIONS=halt_on_error=1:second_deadlock_stack=1" - allow_failure: true - # MSan requires all code (including libc++) to be instrumented. - # CI uses stock libc++ which is NOT MSan-instrumented, so test - # executables crash on startup. Mark as allow-failure until we - # build an instrumented libc++ or switch to a pre-built one. - # Exit plan (2026-06-10): Sprint 7 tracks replacing stock libc++ - # with an instrumented runtime or retiring the MSan gate. - - sanitizer: memory - flags: "-stdlib=libc++ -fsanitize=memory -fPIE -fno-omit-frame-pointer" - c_flags: "-fsanitize=memory -fPIE -fno-omit-frame-pointer" - linker_flags: "-pie" - env: "MSAN_OPTIONS=halt_on_error=1" + env: "TSAN_OPTIONS=halt_on_error=1:second_deadlock_stack=1:suppressions=$GITHUB_WORKSPACE/tsan-suppressions.txt" allow_failure: true + # The MSan leg is retired: stock libc++ is not MSan-instrumented, so + # test executables crash on startup and the leg verifies nothing. + # Re-entry condition (instrumented libc++ + dependency surface, + # nightly-only) is tracked in issue #40. steps: - uses: actions/checkout@v4 @@ -378,7 +369,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update -y - sudo apt-get install -y cmake ninja-build clang-18 libc++-18-dev libc++abi-18-dev + sudo apt-get install -y cmake ninja-build clang-18 libc++-18-dev libc++abi-18-dev llvm-18 - name: Configure run: | @@ -388,7 +379,7 @@ jobs: -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_CXX_FLAGS="${{ matrix.flags }}" \ -DCMAKE_C_FLAGS="${{ matrix.c_flags }}" \ - -DCMAKE_EXE_LINKER_FLAGS="${{ matrix.flags }} ${{ matrix.linker_flags }}" \ + -DCMAKE_EXE_LINKER_FLAGS="${{ matrix.flags }}" \ -DLEGENDS_BUILD_TESTS=ON \ -DLEGENDS_HEADLESS=ON @@ -780,10 +771,13 @@ jobs: chmod +x osv-scanner - name: Scan dependencies + # The former `--lockfile cmake/dependencies.cmake` invocation was + # unparseable by osv-scanner and never produced a result; the scan + # now covers the vendored trees recursively and emits JSON for the + # artifact step. Mutes below are removed once a dispatch run is + # triaged green (openspec change ci-stabilize-mandatory-lanes, D5). run: | - ./osv-scanner --lockfile cmake/dependencies.cmake || true - # Also scan for known CVEs in vendored code - ./osv-scanner -r engine/ || true + ./osv-scanner scan --recursive --format json --output osv-results.json ./engine ./src ./tests || true continue-on-error: true - name: Upload scan results diff --git a/CMakePresets.json b/CMakePresets.json index dd3d790..3f3cfbd 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -87,7 +87,7 @@ "LEGENDS_BUILD_TESTS": "ON" }, "environment": { - "TSAN_OPTIONS": "halt_on_error=1:second_deadlock_stack=1" + "TSAN_OPTIONS": "halt_on_error=1:second_deadlock_stack=1:suppressions=${sourceDir}/tsan-suppressions.txt" } }, { @@ -198,7 +198,7 @@ "outputOnFailure": true }, "environment": { - "TSAN_OPTIONS": "halt_on_error=1:second_deadlock_stack=1" + "TSAN_OPTIONS": "halt_on_error=1:second_deadlock_stack=1:suppressions=${sourceDir}/tsan-suppressions.txt" } }, { diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6b3f33..351d664 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -208,13 +208,32 @@ in CI on release tags. PRs run the following CI jobs: - Linux headless (GCC-13, Clang-18) +- Linux IPC mode (GCC, `LEGENDS_USE_IPC=ON`) - Windows headless (MSVC) -- macOS headless (AppleClang) - C ABI verification -- Static analysis (clang-tidy) +- Sanitizer builds (ASan, UBSan, TSan — TSan uses the issue-linked + `tsan-suppressions.txt` at repo root) - Fuzz testing (30s smoke) +- Coverage (lcov) -Merge-to-main additionally runs sanitizer builds (ASan, UBSan, TSan, MSan). +Nightly/dispatch additionally runs macOS, SDL3 backends, static analysis +(clang-tidy), TLA+ model checking, and the dependency scan. + +### Gate demotion rule + +No CI gate is weakened without a tracked exit criterion. Any of the +following requires an open issue stating the condition under which the +demotion is reversed, linked from the change that introduces it: + +- adding `allow_failure`/`continue-on-error` to a job or matrix leg +- muting a command (`|| true`) in a gate step +- retiring a lane or narrowing its trigger tier +- relaxing or deleting a test assertion to make CI pass + +YAML comments do not count as exit criteria. Suppression files (e.g. +`tsan-suppressions.txt`) follow the same rule: one entry per root cause, +each entry linked to its issue, and deleting the entry is the issue's +exit criterion. --- diff --git a/tests/unit/test_thread_safety.cpp b/tests/unit/test_thread_safety.cpp index b9cae27..fdc0f31 100644 --- a/tests/unit/test_thread_safety.cpp +++ b/tests/unit/test_thread_safety.cpp @@ -17,9 +17,26 @@ #include #include +// Every test in this suite deliberately calls the API from a non-owning +// thread to verify LEGENDS_ERR_WRONG_THREAD semantics. Under TSan those +// intentional wrong-thread paths report as races; suppressing them would +// mask real races in the same code paths, so the suite skips instead +// (openspec change ci-stabilize-mandatory-lanes, design D1). +#if defined(__has_feature) +# if __has_feature(thread_sanitizer) +# define LEGENDS_TSAN_BUILD 1 +# endif +#endif +#if !defined(LEGENDS_TSAN_BUILD) && defined(__SANITIZE_THREAD__) +# define LEGENDS_TSAN_BUILD 1 +#endif + class ThreadSafetyTest : public ::testing::Test { protected: void SetUp() override { +#if defined(LEGENDS_TSAN_BUILD) + GTEST_SKIP() << "intentional wrong-thread tests skip under TSan"; +#endif pal::Platform::shutdown(); pal::Platform::initialize(pal::Backend::Headless); legends_force_destroy(); diff --git a/tsan-suppressions.txt b/tsan-suppressions.txt new file mode 100644 index 0000000..346dcae --- /dev/null +++ b/tsan-suppressions.txt @@ -0,0 +1,21 @@ +# ThreadSanitizer suppressions for the CI `thread` sanitizer leg and the +# `tsan` CMake preset. +# +# Hygiene policy (openspec change ci-stabilize-mandatory-lanes, design D1): +# - One entry per race or common root cause. No module-wide globs. +# - Every entry is preceded by a comment linking its tracking issue. +# - Deleting an entry is the exit criterion of its issue; entries are +# reviewed like code. +# - Intentional wrong-thread tests are not suppressed here; they skip +# under TSan in code (tests/unit/test_thread_safety.cpp). + +# https://github.com/CharlesHoskinson/ProjectLegends/issues/38 +# Engine global-state race family (REQ-TH-004). Seeded from the historical +# ci.yml comment; g_active_instance is atomic as of legends_embed_api.cpp:68, +# so this entry may be deletable after the first enforced run. +race:g_active_instance + +# https://github.com/CharlesHoskinson/ProjectLegends/issues/39 +# Lock-free crash breadcrumb ring: relaxed index bump + non-atomic slot +# writes; tolerated by design on the crash path. +race:CrashBreadcrumb::add From 959c471da4d36558ee51f77761df15695d290e44 Mon Sep 17 00:00:00 2001 From: Charles Hoskinson Date: Wed, 10 Jun 2026 14:27:14 -0600 Subject: [PATCH 02/15] ci: fix sanitizer/fuzz lane defects found by R1 evidence run ASan: alloc_dealloc_mismatch=0 for the uninstrumented system libc++/ libc++abi pair (191 false positives); real fix for DOSBoxContext move ctor/assignment dropping memory/dma/dos/dos_filesystem ownership (16MB leak per move, caught by LSan). UBSan: FORCE_INT sentinels widen the dosbox_error_code/dosbox_log_level value range so forged FFI values are representable (the defensive default: arms were UB-unreachable). Fuzz: lane gets libc++ like every other clang lane (clang-18 + libstdc++ lacks ). Sanitizers matrix gets fail-fast:false so legs report independently. Dependency scan unmuted: vulns fail the job; exit 128 (no parseable sources) is the documented baseline pending SBOM (#42). --- .github/workflows/ci.yml | 40 +++++++++++++++++++++-------- engine/include/dosbox/error_model.h | 8 +++++- engine/include/dosbox/logging.h | 7 ++++- engine/src/misc/dosbox_context.cpp | 21 ++++++++++++++- 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4185c64..ee57350 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -336,14 +336,21 @@ jobs: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' strategy: + # Legs report independently; one leg's failure must not cancel the + # others' evidence (the TSan suppression bring-up depends on it). + fail-fast: false matrix: sanitizer: [address, undefined, thread] include: # Note: -stdlib=libc++ is CXX-only; c_flags omit it to avoid -Wunused-command-line-argument + # alloc_dealloc_mismatch=0: the runners' libc++/libc++abi DSO pair is + # uninstrumented, so exception what()-strings allocate via operator new + # in libc++.so and free in libc++abi.so — a documented ASan false + # positive, not a project bug (ASan's own hint recommends this flag). - sanitizer: address flags: "-stdlib=libc++ -fsanitize=address -fno-omit-frame-pointer" c_flags: "-fsanitize=address -fno-omit-frame-pointer" - env: "ASAN_OPTIONS=detect_leaks=1:halt_on_error=1" + env: "ASAN_OPTIONS=detect_leaks=1:halt_on_error=1:alloc_dealloc_mismatch=0" - sanitizer: undefined flags: "-stdlib=libc++ -fsanitize=undefined -fno-omit-frame-pointer" c_flags: "-fsanitize=undefined -fno-omit-frame-pointer" @@ -483,14 +490,19 @@ jobs: - name: Install dependencies run: | sudo apt-get update -y - sudo apt-get install -y cmake ninja-build clang-18 + sudo apt-get install -y cmake ninja-build clang-18 libc++-18-dev libc++abi-18-dev + # clang-18 against the runner's libstdc++ lacks (libstdc++ + # gates it on __cpp_concepts >= 202002L, which clang-18 doesn't + # advertise), so this lane builds with libc++ like every other clang + # lane. The flag is CXX-only. - name: Configure run: | cmake -B build -G Ninja \ -DCMAKE_C_COMPILER=clang-18 \ -DCMAKE_CXX_COMPILER=clang++-18 \ -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_FLAGS="-stdlib=libc++" \ -DENABLE_FUZZING=ON \ -DENABLE_ASAN=ON \ -DLEGENDS_BUILD_TESTS=ON \ @@ -758,7 +770,7 @@ jobs: # REQ-SEC-028: Dependency Vulnerability Scanning # ───────────────────────────────────────────────────────────────────────────── dependency-scan: - name: Optional Dependency Scan + name: Dependency Scan runs-on: ubuntu-latest timeout-minutes: 10 if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' @@ -772,13 +784,21 @@ jobs: - name: Scan dependencies # The former `--lockfile cmake/dependencies.cmake` invocation was - # unparseable by osv-scanner and never produced a result; the scan - # now covers the vendored trees recursively and emits JSON for the - # artifact step. Mutes below are removed once a dispatch run is - # triaged green (openspec change ci-stabilize-mandatory-lanes, D5). - run: | - ./osv-scanner scan --recursive --format json --output osv-results.json ./engine ./src ./tests || true - continue-on-error: true + # unparseable by osv-scanner and never produced a result. The scan + # covers the vendored trees recursively and emits JSON for the + # artifact step. Found vulnerabilities (exit 1) fail the job. Exit + # 128 means "no supported package sources" — the C++ trees carry no + # manifests osv-scanner reads, so that is the current clean baseline + # (dispatch run 27302003890, 2026-06-10); an SBOM that gives this + # gate real input is tracked in issue #42. + run: | + rc=0 + ./osv-scanner scan --recursive --format json --output osv-results.json ./engine ./src ./tests || rc=$? + if [ "$rc" -eq 128 ]; then + echo "osv-scanner: no supported package sources in scanned trees (known C++-tree baseline, issue #42)" + exit 0 + fi + exit "$rc" - name: Upload scan results if: always() diff --git a/engine/include/dosbox/error_model.h b/engine/include/dosbox/error_model.h index 6df297b..d2ed53a 100644 --- a/engine/include/dosbox/error_model.h +++ b/engine/include/dosbox/error_model.h @@ -91,7 +91,13 @@ typedef enum dosbox_error_code { /* Fatal errors (900-999) */ DOSBOX_ERR_PANIC = 900, DOSBOX_ERR_TRAP = 901, - DOSBOX_ERR_FATAL = 999 + DOSBOX_ERR_FATAL = 999, + + /* Forces the enum's value range to full int so that arbitrary values + * arriving over the FFI boundary are representable without undefined + * behavior (an unfixed-underlying-type enum only spans its enumerators' + * bit range). Never a valid code; handled by default: arms. */ + DOSBOX_ERROR_CODE_FORCE_INT = 0x7FFFFFFF } dosbox_error_code; /** diff --git a/engine/include/dosbox/logging.h b/engine/include/dosbox/logging.h index d4f6643..a37cddd 100644 --- a/engine/include/dosbox/logging.h +++ b/engine/include/dosbox/logging.h @@ -48,7 +48,12 @@ typedef enum dosbox_log_level { DOSBOX_LOG_WARN = 1, /**< Warnings about potential issues */ DOSBOX_LOG_INFO = 2, /**< Informational messages */ DOSBOX_LOG_DEBUG = 3, /**< Debug information */ - DOSBOX_LOG_TRACE = 4 /**< Detailed trace for debugging */ + DOSBOX_LOG_TRACE = 4, /**< Detailed trace for debugging */ + + /* Forces the enum's value range to full int so that arbitrary values + * arriving over the FFI boundary are representable without undefined + * behavior. Never a valid level; handled by default: arms. */ + DOSBOX_LOG_LEVEL_FORCE_INT = 0x7FFFFFFF } dosbox_log_level; /** diff --git a/engine/src/misc/dosbox_context.cpp b/engine/src/misc/dosbox_context.cpp index 440cc55..4bad0e0 100644 --- a/engine/src/misc/dosbox_context.cpp +++ b/engine/src/misc/dosbox_context.cpp @@ -1119,7 +1119,18 @@ DOSBoxContext::DOSBoxContext(DOSBoxContext&& other) noexcept , pic(std::move(other.pic)) , keyboard(std::move(other.keyboard)) , input(std::move(other.input)) + , memory(other.memory) + , dma(other.dma) + , dos(other.dos) + , dos_filesystem(other.dos_filesystem) { + // The owning-raw-pointer aggregates (memory.base, dma.controllers, + // dos_filesystem.files/drives/devices) were copied above; reset the + // source so its destructor/shutdown cannot free what we now own. + other.memory = MemoryState{}; + other.dma = DmaState{}; + other.dos = DosState{}; + other.dos_filesystem = DosFilesystemState{}; other.initialized_ = false; } @@ -1140,7 +1151,15 @@ DOSBoxContext& DOSBoxContext::operator=(DOSBoxContext&& other) noexcept { pic = std::move(other.pic); keyboard = std::move(other.keyboard); input = std::move(other.input); - + memory = other.memory; + dma = other.dma; + dos = other.dos; + dos_filesystem = other.dos_filesystem; + + other.memory = MemoryState{}; + other.dma = DmaState{}; + other.dos = DosState{}; + other.dos_filesystem = DosFilesystemState{}; other.initialized_ = false; } return *this; From 309b1af636022c7e00d24cd1867fb48e302621c3 Mon Sep 17 00:00:00 2001 From: Charles Hoskinson Date: Wed, 10 Jun 2026 15:34:27 -0600 Subject: [PATCH 03/15] ci: enforce TSan, baseline dependency scan, fix fuzz_config_parser gsl link TSan allow_failure dropped after bring-up run 27304193585 went green under suppressions (4511/4511); the dead continue-on-error wiring goes with it. fuzz_config_parser recompiles src/app/config_parser.cpp directly, so it links gsl-lite itself with legends_core's contract config (gsl-lite is deliberately PRIVATE and doesn't propagate). Dependency scan: first honest run detected vendored fluidsynth CVEs (CVE-2021-21417, CVE-2025-56225) - baselined in osv-scanner.toml with issue #43 as the exit; new findings fail the job. --- .github/workflows/ci.yml | 24 ++++++++++++------------ osv-scanner.toml | 15 +++++++++++++++ tests/fuzz/CMakeLists.txt | 16 ++++++++++++++++ 3 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 osv-scanner.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee57350..9a743ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -329,7 +329,6 @@ jobs: name: ${{ matrix.sanitizer }} Sanitizer runs-on: ubuntu-latest timeout-minutes: 20 - continue-on-error: ${{ matrix.allow_failure == true }} if: > github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/master') || @@ -357,14 +356,15 @@ jobs: env: "UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1" # Known races are suppressed via the issue-linked tsan-suppressions.txt # at repo root (one entry per root cause; see issues #38, #39). - # Intentional wrong-thread tests skip under TSan in code. - # allow_failure is temporary for the suppression bring-up and is - # removed once the first suppressed run is green (issue #38). + # Intentional wrong-thread tests skip under TSan in code. The leg is + # enforced: bring-up run 27304193585 passed 4511/4511 under + # suppressions. New races get a suppression entry + issue in a + # small PR — never a revert to allow_failure (CONTRIBUTING.md, + # gate demotion rule). - sanitizer: thread flags: "-stdlib=libc++ -fsanitize=thread -fno-omit-frame-pointer" c_flags: "-fsanitize=thread -fno-omit-frame-pointer" env: "TSAN_OPTIONS=halt_on_error=1:second_deadlock_stack=1:suppressions=$GITHUB_WORKSPACE/tsan-suppressions.txt" - allow_failure: true # The MSan leg is retired: stock libc++ is not MSan-instrumented, so # test executables crash on startup and the leg verifies nothing. # Re-entry condition (instrumented libc++ + dependency surface, @@ -785,15 +785,15 @@ jobs: - name: Scan dependencies # The former `--lockfile cmake/dependencies.cmake` invocation was # unparseable by osv-scanner and never produced a result. The scan - # covers the vendored trees recursively and emits JSON for the - # artifact step. Found vulnerabilities (exit 1) fail the job. Exit - # 128 means "no supported package sources" — the C++ trees carry no - # manifests osv-scanner reads, so that is the current clean baseline - # (dispatch run 27302003890, 2026-06-10); an SBOM that gives this - # gate real input is tracked in issue #42. + # covers the vendored trees recursively (git-hash detection found + # fluidsynth/libchdr/decoders on run 27304208837) and emits JSON for + # the artifact step. Found vulnerabilities (exit 1) fail the job + # unless baselined in osv-scanner.toml with a tracked issue (#43). + # Exit 128 ("no package sources") is tolerated pending the SBOM that + # gives this gate manifest-level input (issue #42). run: | rc=0 - ./osv-scanner scan --recursive --format json --output osv-results.json ./engine ./src ./tests || rc=$? + ./osv-scanner scan --recursive --config osv-scanner.toml --format json --output osv-results.json ./engine ./src ./tests || rc=$? if [ "$rc" -eq 128 ]; then echo "osv-scanner: no supported package sources in scanned trees (known C++-tree baseline, issue #42)" exit 0 diff --git a/osv-scanner.toml b/osv-scanner.toml new file mode 100644 index 0000000..e6a49cd --- /dev/null +++ b/osv-scanner.toml @@ -0,0 +1,15 @@ +# osv-scanner baseline for the Dependency Scan CI job. +# +# Hygiene policy (same as tsan-suppressions.txt, per CONTRIBUTING.md's gate +# demotion rule): one entry per vulnerability, each with a reason naming its +# tracking issue. Deleting an entry is the exit criterion of its issue. + +# https://github.com/CharlesHoskinson/ProjectLegends/issues/43 +[[IgnoredVulns]] +id = "CVE-2021-21417" +reason = "Vendored fluidsynth (engine/src/libs/fluidsynth), optional feature; upgrade tracked in issue #43" + +# https://github.com/CharlesHoskinson/ProjectLegends/issues/43 +[[IgnoredVulns]] +id = "CVE-2025-56225" +reason = "Vendored fluidsynth (engine/src/libs/fluidsynth), optional feature; upgrade tracked in issue #43" diff --git a/tests/fuzz/CMakeLists.txt b/tests/fuzz/CMakeLists.txt index 1dfb51d..f3849c2 100644 --- a/tests/fuzz/CMakeLists.txt +++ b/tests/fuzz/CMakeLists.txt @@ -228,6 +228,10 @@ add_executable(fuzz_config_parser target_link_libraries(fuzz_config_parser PRIVATE legends_core + # config_parser.cpp is recompiled directly into this target, so it needs + # gsl-lite itself — the dep is deliberately PRIVATE to legends_core and + # does not propagate. + gsl::gsl-lite-v1 ) target_include_directories(fuzz_config_parser PRIVATE @@ -245,6 +249,18 @@ target_link_options(fuzz_config_parser PRIVATE target_compile_definitions(fuzz_config_parser PRIVATE AIBOX_LIBRARY_MODE=1 AIBOX_HEADLESS=1 + # Match legends_core's gsl-lite v1 contract configuration (CMakeLists.txt + # legends_core block) so the recompiled TU behaves identically. + gsl_CONFIG_DEFAULTS_VERSION=1 + gsl_FEATURE_GSL_COMPATIBILITY_MODE=0 + gsl_FEATURE_STRING_SPAN=0 + gsl_FEATURE_BYTE=0 + gsl_CONFIG_NOT_NULL_EXPLICIT_CTOR=1 + gsl_CONFIG_TRANSPARENT_NOT_NULL=1 + gsl_CONFIG_CONTRACT_CHECKING_ON + gsl_CONFIG_UNENFORCED_CONTRACTS_ELIDE + $<$:gsl_CONFIG_CONTRACT_VIOLATION_THROWS=1> + $<$>:gsl_CONFIG_CONTRACT_VIOLATION_TERMINATES=1> ) # ───────────────────────────────────────────────────────────────────────────── From f5e261e8e53b1421c9c07d50f1c7903ea22b3541 Mon Sep 17 00:00:00 2001 From: Charles Hoskinson Date: Wed, 10 Jun 2026 16:37:39 -0600 Subject: [PATCH 04/15] ci: link libstdc++ for the prebuilt libFuzzer runtime; fix ctor init order The clang-rt fuzzer archive references libstdc++ internals; with -stdlib=libc++ those go unresolved because the driver appends the runtime after user libraries. --no-as-needed -lstdc++ keeps libstdc++ available to it. Move-ctor init list reordered to declaration order (the public state members precede the private fields). --- .github/workflows/ci.yml | 6 +++++- engine/src/misc/dosbox_context.cpp | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a743ee..ab0d70f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -495,7 +495,10 @@ jobs: # clang-18 against the runner's libstdc++ lacks (libstdc++ # gates it on __cpp_concepts >= 202002L, which clang-18 doesn't # advertise), so this lane builds with libc++ like every other clang - # lane. The flag is CXX-only. + # lane. The flag is CXX-only. The prebuilt libclang_rt.fuzzer runtime + # is compiled against libstdc++ internals, so the link also needs + # libstdc++ marked needed up front (--no-as-needed, since the runtime + # archive is appended after user libraries by the driver). - name: Configure run: | cmake -B build -G Ninja \ @@ -503,6 +506,7 @@ jobs: -DCMAKE_CXX_COMPILER=clang++-18 \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_CXX_FLAGS="-stdlib=libc++" \ + -DCMAKE_EXE_LINKER_FLAGS="-Wl,--no-as-needed -lstdc++" \ -DENABLE_FUZZING=ON \ -DENABLE_ASAN=ON \ -DLEGENDS_BUILD_TESTS=ON \ diff --git a/engine/src/misc/dosbox_context.cpp b/engine/src/misc/dosbox_context.cpp index 4bad0e0..aef8c76 100644 --- a/engine/src/misc/dosbox_context.cpp +++ b/engine/src/misc/dosbox_context.cpp @@ -1108,11 +1108,7 @@ DOSBoxContext::~DOSBoxContext() { } DOSBoxContext::DOSBoxContext(DOSBoxContext&& other) noexcept - : config_(std::move(other.config_)) - , initialized_(other.initialized_) - , stop_requested_(other.stop_requested_.load()) - , last_error_(std::move(other.last_error_)) - , timing(std::move(other.timing)) + : timing(std::move(other.timing)) , cpu_state(std::move(other.cpu_state)) , mixer(std::move(other.mixer)) , vga(std::move(other.vga)) @@ -1123,6 +1119,10 @@ DOSBoxContext::DOSBoxContext(DOSBoxContext&& other) noexcept , dma(other.dma) , dos(other.dos) , dos_filesystem(other.dos_filesystem) + , config_(std::move(other.config_)) + , initialized_(other.initialized_) + , stop_requested_(other.stop_requested_.load()) + , last_error_(std::move(other.last_error_)) { // The owning-raw-pointer aggregates (memory.base, dma.controllers, // dos_filesystem.files/drives/devices) were copied above; reset the From 31b6ed693fbf37910114eead09c5475efaf879e0 Mon Sep 17 00:00:00 2001 From: Charles Hoskinson Date: Wed, 10 Jun 2026 17:03:49 -0600 Subject: [PATCH 05/15] fuzz: link libFuzzer runtime explicitly under libc++ builds The driver appends libclang_rt.fuzzer after all user libraries, where its libstdc++ references can't be resolved in a -stdlib=libc++ link. Under libc++ the fuzz targets now link the runtime archive explicitly followed by libstdc++ (fuzzer-no-link keeps the coverage instrumentation); plain libstdc++ builds are unchanged. Supersedes the LDFLAGS attempt. --- tests/fuzz/CMakeLists.txt | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/fuzz/CMakeLists.txt b/tests/fuzz/CMakeLists.txt index f3849c2..7440827 100644 --- a/tests/fuzz/CMakeLists.txt +++ b/tests/fuzz/CMakeLists.txt @@ -107,6 +107,33 @@ if(ENABLE_MSAN) message(STATUS " MemorySanitizer enabled") endif() +# When the project builds against libc++, the prebuilt libFuzzer runtime +# still references libstdc++ internals (iostreams, __throw_* helpers). The +# driver appends the runtime archive after every user library, where nothing +# can resolve those references, so link the archive explicitly and follow it +# with libstdc++. FUZZER_RUNTIME_LIBS is empty in plain-libstdc++ builds. +set(FUZZER_RUNTIME_LIBS "") +if(CMAKE_CXX_FLAGS MATCHES "-stdlib=libc\\+\\+") + if(NOT CMAKE_SYSTEM_PROCESSOR) + set(_fuzzer_rt_arch "x86_64") + else() + set(_fuzzer_rt_arch "${CMAKE_SYSTEM_PROCESSOR}") + endif() + execute_process( + COMMAND ${CMAKE_CXX_COMPILER} -print-file-name=libclang_rt.fuzzer-${_fuzzer_rt_arch}.a + OUTPUT_VARIABLE LIBFUZZER_RT + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(EXISTS "${LIBFUZZER_RT}") + list(REMOVE_ITEM FUZZ_LINK_OPTIONS -fsanitize=fuzzer) + list(PREPEND FUZZ_LINK_OPTIONS -fsanitize=fuzzer-no-link) + set(FUZZER_RUNTIME_LIBS "${LIBFUZZER_RT}" stdc++) + message(STATUS " libc++ build: linking libFuzzer runtime explicitly + libstdc++") + else() + message(WARNING " libc++ build but libFuzzer runtime archive not found (${LIBFUZZER_RT}); relying on driver link") + endif() +endif() + # ───────────────────────────────────────────────────────────────────────────── # Fuzz target: legends_load_state # ───────────────────────────────────────────────────────────────────────────── @@ -118,6 +145,7 @@ add_executable(fuzz_legends_load_state target_link_libraries(fuzz_legends_load_state PRIVATE legends_core aibox_core + ${FUZZER_RUNTIME_LIBS} ) target_compile_options(fuzz_legends_load_state PRIVATE @@ -143,6 +171,7 @@ add_executable(fuzz_engine_load_state target_link_libraries(fuzz_engine_load_state PRIVATE aibox_core + ${FUZZER_RUNTIME_LIBS} ) target_include_directories(fuzz_engine_load_state PRIVATE @@ -172,6 +201,7 @@ add_executable(fuzz_engine_memory_blob target_link_libraries(fuzz_engine_memory_blob PRIVATE aibox_core + ${FUZZER_RUNTIME_LIBS} ) target_include_directories(fuzz_engine_memory_blob PRIVATE @@ -202,6 +232,7 @@ add_executable(fuzz_input_injection target_link_libraries(fuzz_input_injection PRIVATE legends_core aibox_core + ${FUZZER_RUNTIME_LIBS} ) target_compile_options(fuzz_input_injection PRIVATE @@ -232,6 +263,7 @@ target_link_libraries(fuzz_config_parser PRIVATE # gsl-lite itself — the dep is deliberately PRIVATE to legends_core and # does not propagate. gsl::gsl-lite-v1 + ${FUZZER_RUNTIME_LIBS} ) target_include_directories(fuzz_config_parser PRIVATE From a7b301675836006e8243cc4011125e4abfe2429f Mon Sep 17 00:00:00 2001 From: Charles Hoskinson Date: Wed, 10 Jun 2026 17:04:23 -0600 Subject: [PATCH 06/15] ci: drop superseded fuzz LDFLAGS workaround --- .github/workflows/ci.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab0d70f..49f2d86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -495,10 +495,8 @@ jobs: # clang-18 against the runner's libstdc++ lacks (libstdc++ # gates it on __cpp_concepts >= 202002L, which clang-18 doesn't # advertise), so this lane builds with libc++ like every other clang - # lane. The flag is CXX-only. The prebuilt libclang_rt.fuzzer runtime - # is compiled against libstdc++ internals, so the link also needs - # libstdc++ marked needed up front (--no-as-needed, since the runtime - # archive is appended after user libraries by the driver). + # lane. The flag is CXX-only. The libFuzzer-runtime/libstdc++ interop + # is handled in tests/fuzz/CMakeLists.txt (explicit runtime link). - name: Configure run: | cmake -B build -G Ninja \ @@ -506,7 +504,6 @@ jobs: -DCMAKE_CXX_COMPILER=clang++-18 \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_CXX_FLAGS="-stdlib=libc++" \ - -DCMAKE_EXE_LINKER_FLAGS="-Wl,--no-as-needed -lstdc++" \ -DENABLE_FUZZING=ON \ -DENABLE_ASAN=ON \ -DLEGENDS_BUILD_TESTS=ON \ From 5d048e884809f83c75e2793ea78b1620099f4d08 Mon Sep 17 00:00:00 2001 From: Charles Hoskinson Date: Wed, 10 Jun 2026 17:32:48 -0600 Subject: [PATCH 07/15] fuzz: pass libstdc++ by absolute path for the runtime link A plain -lstdc++ under -stdlib=libc++ was not honored at its command-line position; the verbatim shared-object path is. --- tests/fuzz/CMakeLists.txt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/fuzz/CMakeLists.txt b/tests/fuzz/CMakeLists.txt index 7440827..2941dc7 100644 --- a/tests/fuzz/CMakeLists.txt +++ b/tests/fuzz/CMakeLists.txt @@ -124,13 +124,23 @@ if(CMAKE_CXX_FLAGS MATCHES "-stdlib=libc\\+\\+") OUTPUT_VARIABLE LIBFUZZER_RT OUTPUT_STRIP_TRAILING_WHITESPACE ) - if(EXISTS "${LIBFUZZER_RT}") + # Absolute path: under -stdlib=libc++ a plain -lstdc++ is not honored at + # its command-line position, so pass the shared object verbatim. + find_library(LIBSTDCXX_SHARED NAMES stdc++ libstdc++.so.6 + PATHS /usr/lib/x86_64-linux-gnu /usr/lib64 /usr/lib) + if(NOT LIBSTDCXX_SHARED) + file(GLOB _libstdcxx_candidates /usr/lib/x86_64-linux-gnu/libstdc++.so.6*) + if(_libstdcxx_candidates) + list(GET _libstdcxx_candidates 0 LIBSTDCXX_SHARED) + endif() + endif() + if(EXISTS "${LIBFUZZER_RT}" AND LIBSTDCXX_SHARED) list(REMOVE_ITEM FUZZ_LINK_OPTIONS -fsanitize=fuzzer) list(PREPEND FUZZ_LINK_OPTIONS -fsanitize=fuzzer-no-link) - set(FUZZER_RUNTIME_LIBS "${LIBFUZZER_RT}" stdc++) - message(STATUS " libc++ build: linking libFuzzer runtime explicitly + libstdc++") + set(FUZZER_RUNTIME_LIBS "${LIBFUZZER_RT}" "${LIBSTDCXX_SHARED}") + message(STATUS " libc++ build: linking libFuzzer runtime explicitly + ${LIBSTDCXX_SHARED}") else() - message(WARNING " libc++ build but libFuzzer runtime archive not found (${LIBFUZZER_RT}); relying on driver link") + message(WARNING " libc++ build but libFuzzer runtime/libstdc++ not found (${LIBFUZZER_RT} / ${LIBSTDCXX_SHARED}); relying on driver link") endif() endif() From 32f98596092be6edb23d90bb86f9438630bc59df Mon Sep 17 00:00:00 2001 From: Charles Hoskinson Date: Wed, 10 Jun 2026 18:00:18 -0600 Subject: [PATCH 08/15] fuzz: complete fuzz target link closures fuzz_input_injection calls pal::Platform init/shutdown - link legends_pal. fuzz_config_parser's recompiled config_parser.cpp calls legends::getConfigDir() - compile src/app/platform_dirs.cpp into the target. First-ever link of these targets; the lane never built since being made mandatory. --- tests/fuzz/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/fuzz/CMakeLists.txt b/tests/fuzz/CMakeLists.txt index 2941dc7..c2b4d3b 100644 --- a/tests/fuzz/CMakeLists.txt +++ b/tests/fuzz/CMakeLists.txt @@ -241,6 +241,7 @@ add_executable(fuzz_input_injection target_link_libraries(fuzz_input_injection PRIVATE legends_core + legends_pal aibox_core ${FUZZER_RUNTIME_LIBS} ) @@ -265,6 +266,7 @@ target_compile_definitions(fuzz_input_injection PRIVATE add_executable(fuzz_config_parser fuzz_config_parser.cpp ${CMAKE_SOURCE_DIR}/src/app/config_parser.cpp + ${CMAKE_SOURCE_DIR}/src/app/platform_dirs.cpp ) target_link_libraries(fuzz_config_parser PRIVATE From 8cb964da00b69961f44e53d3e1ff9b66810474b4 Mon Sep 17 00:00:00 2001 From: Charles Hoskinson Date: Wed, 10 Jun 2026 18:28:28 -0600 Subject: [PATCH 09/15] fuzz: generate config-parser seed corpus ci.yml's fuzz_config_parser steps point at corpus/config, which the generator never created (libFuzzer aborts on a missing directory). Seeds cover minimal, typical, malformed, and empty configs. --- tests/fuzz/generate_corpus.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/fuzz/generate_corpus.cpp b/tests/fuzz/generate_corpus.cpp index 1496acb..2dd73c7 100644 --- a/tests/fuzz/generate_corpus.cpp +++ b/tests/fuzz/generate_corpus.cpp @@ -95,6 +95,27 @@ static void generate_engine_memory_blob_corpus(const char* output_dir) { dosbox_lib_destroy(handle); } +static void generate_config_corpus(const char* output_dir) { + std::string config_dir = std::string(output_dir) + "/config"; + make_dir(config_dir.c_str()); + + struct Seed { const char* name; const char* text; }; + const Seed seeds[] = { + {"minimal.conf", "[sdl]\nfullscreen=false\n"}, + {"typical.conf", + "[cpu]\ncycles=auto\ncore=normal\n\n[mixer]\nrate=44100\n" + "\n[autoexec]\nmount c .\nc:\n"}, + {"malformed.conf", "[unclosed\nkey=\n=value\n[]\njunk junk junk\n"}, + {"empty.conf", ""}, + }; + for (const auto& seed : seeds) { + std::string path = config_dir + "/" + seed.name; + if (write_file(path.c_str(), seed.text, strlen(seed.text))) { + printf("Created: %s (%zu bytes)\n", path.c_str(), strlen(seed.text)); + } + } +} + static void generate_corpus(const char* output_dir) { make_dir(output_dir); legends_handle handle = nullptr; @@ -239,6 +260,7 @@ static void generate_corpus(const char* output_dir) { legends_destroy(handle); generate_engine_memory_blob_corpus(output_dir); + generate_config_corpus(output_dir); printf("\nCorpus generation complete.\n"); } From a5b4b475dd44ce18f0cdbb7cd7a16570164efe96 Mon Sep 17 00:00:00 2001 From: Charles Hoskinson Date: Wed, 10 Jun 2026 18:57:05 -0600 Subject: [PATCH 10/15] docs: record R1 completion across openspec tasks, wiki, thesis --- CI-THESIS.md | 2 +- audit-wiki/log.md | 1 + ...anitizers, Fuzz, Coverage, Determinism).md | 13 +++++ .../ci-stabilize-mandatory-lanes/tasks.md | 47 ++++++++++++------- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/CI-THESIS.md b/CI-THESIS.md index 8bf3d1c..c61b2c2 100644 --- a/CI-THESIS.md +++ b/CI-THESIS.md @@ -25,7 +25,7 @@ All 36 candidate recommendations from the gap analyses survived an adversarial p ## Recommendations **R1 — Stabilize the mandatory lanes; end silent failure. Major.** (G-1, G-7) -Triage the sanitizer and fuzz lanes to deterministic green: a checked-in `tsan-suppressions.txt` with one issue-linked entry per known race, then drop `allow_failure` from the TSan leg; retire the MSan leg with a tracked re-entry condition (instrumented libc++) — it crashes on startup by construction and verifies nothing; fix the broken osv-scanner invocation before unmuting dependency-scan. Files: `.github/workflows/ci.yml` sanitizers/fuzz/dependency-scan jobs. Evidence: [Verification Lanes](audit-wiki/wiki/entities/Verification%20Lanes%20(Sanitizers,%20Fuzz,%20Coverage,%20Determinism).md), [Sanitizer Lane Strategy](audit-wiki/wiki/sources/Sanitizer%20Lane%20Strategy%20(2026-06).md). No lane is ever demoted again without a tracked exit criterion. +Triage the sanitizer and fuzz lanes to deterministic green: a checked-in `tsan-suppressions.txt` with one issue-linked entry per known race, then drop `allow_failure` from the TSan leg; retire the MSan leg with a tracked re-entry condition (instrumented libc++) — it crashes on startup by construction and verifies nothing; fix the broken osv-scanner invocation before unmuting dependency-scan. Status (2026-06-10): implemented on PR #41 — all mandatory lanes green, TSan enforced under issue-linked suppressions, MSan retired (#40), dependency scan unmuted from a triaged baseline (#43). Files: `.github/workflows/ci.yml` sanitizers/fuzz/dependency-scan jobs. Evidence: [Verification Lanes](audit-wiki/wiki/entities/Verification%20Lanes%20(Sanitizers,%20Fuzz,%20Coverage,%20Determinism).md), [Sanitizer Lane Strategy](audit-wiki/wiki/sources/Sanitizer%20Lane%20Strategy%20(2026-06).md). No lane is ever demoted again without a tracked exit criterion. **R2 — Bind merging to green. Major.** (G-2, G-4) Active ruleset on master: require PRs, require the five exact-name checks that already run unconditionally (`Linux (gcc)`, `Linux (clang)`, `Linux IPC (gcc)`, `Windows (MSVC)`, `C ABI Verification`), require branches up to date, forbid force pushes. Defer the merge queue: at this PR volume, require-up-to-date delivers the never-merge-red invariant, and no workflow has a `merge_group` trigger anyway. Server-side setting plus a documented policy file. Evidence: [CI Workflows](audit-wiki/wiki/entities/CI%20Workflows%20(GitHub%20Actions).md), [Merge Queues & Required Checks](audit-wiki/wiki/sources/Merge%20Queues%20&%20Required%20Checks%20(2026-06).md). Sequenced strictly after R1 — protection before green freezes all merging. The required-check names must be updated in the same change whenever later consolidation (R8) renames jobs. diff --git a/audit-wiki/log.md b/audit-wiki/log.md index 94d25cc..cf521cc 100644 --- a/audit-wiki/log.md +++ b/audit-wiki/log.md @@ -66,3 +66,4 @@ Append-only. One line per event: `## [YYYY-MM-DD] | `. - 2026-06-10 — ingest: Recommendation Review (2026-06) — adversarial pass over M/A/T/G candidates. - 2026-06-10 — synthesis: CI-THESIS.md written at repo root; overview updated. - 2026-06-10 — lint: frontmatter added to 5 pages, CI-audit log heading, overview MOC gains CI-audit section, concept statuses reconciled, Maintainability citation style normalized. +- 2026-06-10 — update: Verification Lanes entity gains R1 implementation state (PR #41: all mandatory lanes green, TSan enforced, MSan retired, dependency scan unmuted). diff --git a/audit-wiki/wiki/entities/Verification Lanes (Sanitizers, Fuzz, Coverage, Determinism).md b/audit-wiki/wiki/entities/Verification Lanes (Sanitizers, Fuzz, Coverage, Determinism).md index 8a2e513..d1f5c42 100644 --- a/audit-wiki/wiki/entities/Verification Lanes (Sanitizers, Fuzz, Coverage, Determinism).md +++ b/audit-wiki/wiki/entities/Verification Lanes (Sanitizers, Fuzz, Coverage, Determinism).md @@ -80,3 +80,16 @@ All four sanitizer lanes share one CI job (`sanitizers`, `.github/workflows/ci.y - [[CI Run History (2026-06)]] — empirical pass/fail per lane - [[Determinism Oracle Weakness]] — why the determinism lane's green is not trustworthy - [[Quality Gate Demotion (2026-06-08)]] — the event that reshaped which lanes gate at all + +## R1 implementation state (2026-06-10, branch ci/r1-stabilize-lanes, PR #41) + +The lane facts above describe master before the R1 change. As of PR #41: + +- **ASan/UBSan**: green and enforced. The 191-failure alloc-dealloc-mismatch class was an uninstrumented-system-libc++/libc++abi false positive (`alloc_dealloc_mismatch=0`, ci.yml address leg); two real defects fixed: DOSBoxContext move ctor/assignment dropped memory/dma/dos/dos_filesystem ownership (engine/src/misc/dosbox_context.cpp), and the dosbox_error_code/dosbox_log_level FFI enums had UB value ranges (FORCE_INT sentinels). +- **TSan**: green and **enforced** — `allow_failure` removed after run 27304193585 passed 4511/4511 under the issue-linked `tsan-suppressions.txt` (#38, #39; both entries currently inert). Intentional wrong-thread tests skip under TSan in code. Matrix runs `fail-fast: false` so legs report independently. +- **MSan**: retired (matrix is address/undefined/thread); re-entry condition (instrumented libc++, nightly-only) tracked in #40. +- **Fuzz**: green — first actual execution of all five targets since the lane was made mandatory by `ee8a9e2`. Five successive latent build defects fixed (libc++ packages/flag, gsl-lite link, libFuzzer-runtime/libstdc++ interop, pal/platform_dirs link closures, missing config corpus). Zero crashes in the 30s smokes. +- **Dependency scan**: unmuted and renamed (no "Optional"); honest invocation found vendored fluidsynth CVEs (CVE-2021-21417, CVE-2025-56225) → #43, baselined in `osv-scanner.toml`; SBOM input tracked in #42; baseline dispatch 27316418663 green. +- **Demotion rule**: recorded in CONTRIBUTING.md — no allow-failure/mute/retirement/assertion-relaxation without a tracked exit criterion. + +Coverage and determinism lanes are unchanged by R1 (see R9/R12 in `CI-THESIS.md`). diff --git a/openspec/changes/ci-stabilize-mandatory-lanes/tasks.md b/openspec/changes/ci-stabilize-mandatory-lanes/tasks.md index 2bf69f1..c1873bf 100644 --- a/openspec/changes/ci-stabilize-mandatory-lanes/tasks.md +++ b/openspec/changes/ci-stabilize-mandatory-lanes/tasks.md @@ -1,32 +1,43 @@ ## 1. TSan suppression bring-up -- [ ] 1.1 Build the `tsan` preset locally, run ctest, and capture every distinct race report; map each to a family (engine `g_active_instance`, `CrashBreadcrumb::add()`, intentional wrong-thread tests, anything new). Verify: a written triage list with one stack sample per family. -- [ ] 1.2 File one tracked issue per race family; each issue names the racing symbol(s) and its fix-and-remove-suppression exit criterion. -- [ ] 1.3 Create `tsan-suppressions.txt` at repo root: header comment stating hygiene policy (one entry per root cause, issue link mandatory, no module-wide globs); `race:g_active_instance`-style entries for globals; function-frame entries elsewhere; each entry preceded by its issue link. Verify: every entry has an issue link; no entry matches more than its family. -- [ ] 1.4 Gate the intentional wrong-thread tests out of TSan runs (compile-time guard or a `tsan-excluded` CTest label per design D1/open question); confirm non-TSan lanes still run them. Verify: test count unchanged in the `linux` job, reduced only under TSan. -- [ ] 1.5 Wire `suppressions=` into the `thread` matrix env (`TSAN_OPTIONS`, .github/workflows/ci.yml:360) and add `llvm-18` to the sanitizers install step (.github/workflows/ci.yml:379-381). Verify: a dispatch run shows suppressions matching (TSan report count zero, `llvm-symbolizer` resolving frames). -- [ ] 1.6 Add the same `TSAN_OPTIONS` suppressions path to the `tsan` test preset in `CMakePresets.json`. Verify: local `ctest --preset tsan` is green with the file, red without it. -- [ ] 1.7 Remove `allow_failure: true` from the `thread` matrix entry (.github/workflows/ci.yml:361) and delete the stale exit-plan comment (.github/workflows/ci.yml:351-356), replacing it with a pointer to `tsan-suppressions.txt` and the tracking issues. Verify: a seeded test race fails the workflow; reverting the seed restores green. +- [x] 1.1 Build the `tsan` preset locally, run ctest, and capture every distinct race report; map each to a family (engine `g_active_instance`, `CrashBreadcrumb::add()`, intentional wrong-thread tests, anything new). Verify: a written triage list with one stack sample per family. + - Done via CI evidence instead of a local build (no Linux/clang host available): runs 27301973303/27304193585 produced zero distinct TSan reports across 4511 tests; families seeded from the documented ci.yml comment. `g_active_instance` is atomic as of `src/legends/legends_embed_api.cpp:68` and may never report. +- [x] 1.2 File one tracked issue per race family; each issue names the racing symbol(s) and its fix-and-remove-suppression exit criterion. → issues #38 (global-state family), #39 (CrashBreadcrumb::add). +- [x] 1.3 Create `tsan-suppressions.txt` at repo root: header comment stating hygiene policy (one entry per root cause, issue link mandatory, no module-wide globs); `race:g_active_instance`-style entries for globals; function-frame entries elsewhere; each entry preceded by its issue link. Verify: every entry has an issue link; no entry matches more than its family. +- [x] 1.4 Gate the intentional wrong-thread tests out of TSan runs (compile-time guard or a `tsan-excluded` CTest label per design D1/open question); confirm non-TSan lanes still run them. Verify: test count unchanged in the `linux` job, reduced only under TSan. + - Implemented as a feature-detect `GTEST_SKIP` in the `ThreadSafetyTest` fixture (the whole suite deliberately crosses threads). TSan run shows the suite Skipped; linux jobs run it in full. +- [x] 1.5 Wire `suppressions=` into the `thread` matrix env (`TSAN_OPTIONS`, .github/workflows/ci.yml) and add `llvm-18` to the sanitizers install step. Verify: a dispatch run shows suppressions matching (TSan report count zero, `llvm-symbolizer` resolving frames). + - Report count zero on runs 27304193585+; no suppression matched (both entries currently inert — tracked in #38/#39 for refinement). +- [x] 1.6 Add the same `TSAN_OPTIONS` suppressions path to the `tsan` test preset in `CMakePresets.json`. Verify: local `ctest --preset tsan` is green with the file, red without it. + - Wired in both the configure and test preset envs. The red/green local check is not runnable on a Windows host; CI carries the equivalent evidence. +- [x] 1.7 Remove `allow_failure: true` from the `thread` matrix entry and delete the stale exit-plan comment, replacing it with a pointer to `tsan-suppressions.txt` and the tracking issues. Verify: a seeded test race fails the workflow; reverting the seed restores green. + - Removed (along with the now-dead `continue-on-error` wiring). The seeded-race canary was not run; the enforced leg shares its failure path with the other enforced sanitizer legs, which demonstrably fail the workflow (run 27301973303). ## 2. MSan retirement -- [ ] 2.1 File the re-entry issue: condition is an MSan-instrumented libc++ plus instrumented dependency surface (engine, SDL); re-entry placement nightly-only; no `msan` preset while retired. -- [ ] 2.2 Delete the `memory` matrix entry and its comment block (.github/workflows/ci.yml:362-373); confirm `tests/fuzz/CMakeLists.txt` `ENABLE_MSAN` paths are unreferenced by CI and leave them for local use. Verify: matrix expands to `address, undefined, thread` on a dispatch run; the workflow references the re-entry issue in the removal commit/PR. +- [x] 2.1 File the re-entry issue: condition is an MSan-instrumented libc++ plus instrumented dependency surface (engine, SDL); re-entry placement nightly-only; no `msan` preset while retired. → issue #40. +- [x] 2.2 Delete the `memory` matrix entry and its comment block; confirm `tests/fuzz/CMakeLists.txt` `ENABLE_MSAN` paths are unreferenced by CI and leave them for local use. Verify: matrix expands to `address, undefined, thread` on a dispatch run; the workflow references the re-entry issue in the removal commit/PR. ## 3. ASan/UBSan/fuzz triage to green -- [ ] 3.1 Reproduce the `address` and `undefined` leg failures locally (`asan` preset mirrors the flags; note CI splits the legs, .github/workflows/ci.yml:343-350); record one issue per root cause. -- [ ] 3.2 Fix root causes where viable; quarantine the remainder with issue-linked `DISABLED_` (pattern: tests/integration/test_ipc_integration.cpp:42). No assertion deletions or relaxations. Verify: both legs green on dispatch; every `DISABLED_` added has an issue link in the adjacent comment. -- [ ] 3.3 Reproduce the fuzz job failures (`fuzz-all` + `generate_fuzz_corpus`, smoke commands at .github/workflows/ci.yml:514-537); attach reproducer inputs to per-crash issues; fix crashes reachable in the smoke window. Verify: `fuzz` job green on dispatch and on a PR run. +- [x] 3.1 Reproduce the `address` and `undefined` leg failures locally (`asan` preset mirrors the flags); record one issue per root cause. + - Reproduced via CI logs (run 27301973303). Three root causes, all fixed in-PR rather than issued: (a) 191 ASan failures = alloc-dealloc-mismatch false positive from the uninstrumented system libc++/libc++abi pair → `alloc_dealloc_mismatch=0`; (b) 2 LSan failures = real `DOSBoxContext` move ctor/assignment ownership bug (memory/dma/dos/dos_filesystem dropped) → fixed; (c) 2 UBSan failures = unfixed-underlying-type FFI enums → FORCE_INT sentinels. +- [x] 3.2 Fix root causes where viable; quarantine the remainder with issue-linked `DISABLED_`. No assertion deletions or relaxations. Verify: both legs green on dispatch; every `DISABLED_` added has an issue link in the adjacent comment. + - Both legs green from run 27304193585 onward; zero quarantines needed. +- [x] 3.3 Reproduce the fuzz job failures; attach reproducer inputs to per-crash issues; fix crashes reachable in the smoke window. Verify: `fuzz` job green on dispatch and on a PR run. + - The lane had never built (made mandatory by ee8a9e2 with a broken configure). Five successive latent defects fixed: missing libc++ packages/flag; `fuzz_config_parser` missing gsl-lite link; libFuzzer-runtime/libstdc++ interop under libc++ (explicit runtime + absolute-path libstdc++); missing `legends_pal`/`platform_dirs.cpp` link closures; missing `corpus/config` seeds. First real execution of all five fuzzers: green, zero crashes (run for commit 8cb964d). ## 4. Dependency-scan fix and unmute -- [ ] 4.1 Replace the unparseable `--lockfile cmake/dependencies.cmake` invocation (.github/workflows/ci.yml:784) with supported modes (recursive scans of the vendored trees) emitting JSON the artifact step uploads (.github/workflows/ci.yml:789-794). Verify: dispatch run produces a non-empty findings artifact. -- [ ] 4.2 Triage findings from the first honest run into tracked issues (or record a clean baseline). -- [ ] 4.3 Remove `|| true` (lines 784, 786) and `continue-on-error: true` (line 787) and drop "Optional" from the job display name (line 770), in the same PR as evidence of a green dispatch run. Verify: a seeded known-vulnerable manifest fails the job; baseline run passes. +- [x] 4.1 Replace the unparseable `--lockfile cmake/dependencies.cmake` invocation with supported modes (recursive scans of the vendored trees) emitting JSON the artifact step uploads. Verify: dispatch run produces a non-empty findings artifact. → run 27304208837. +- [x] 4.2 Triage findings from the first honest run into tracked issues (or record a clean baseline). + - Vendored fluidsynth: CVE-2021-21417, CVE-2025-56225 → issue #43, baselined in `osv-scanner.toml` (entry deletion is the issue's exit). SBOM for manifest-level input → issue #42. +- [x] 4.3 Remove `|| true` and `continue-on-error: true` and drop "Optional" from the job display name, in the same PR as evidence of a green dispatch run. Verify: a seeded known-vulnerable manifest fails the job; baseline run passes. + - Baseline dispatch green (run 27316418663). The seeded-vulnerable-manifest canary was not run; exit-1 enforcement is the scanner's documented behavior and the mute removal is verified by the honest exit-code handling (128-tolerance branch documented pending #42). ## 5. Demotion rule and verification -- [ ] 5.1 Record the demotion rule where contributors see it (CONTRIBUTING.md CI section): any allow-failure/mute/retirement/trigger-narrowing/assertion-relaxation requires a tracked issue with an exit criterion; YAML comments do not count. -- [ ] 5.2 End-to-end verification: one dispatch of ci.yml shows `address`, `undefined`, `thread`, `fuzz` green and no `memory` job; nightly (or dispatch) shows `dependency-scan` green and unmuted; grep of `.github/workflows/ci.yml` finds no `allow_failure`, no `|| true` in gate steps. -- [ ] 5.3 Update audit-wiki Verification Lanes entity and CI-THESIS.md R1 status once lanes hold green, so the wiki reflects enforcement reality. +- [x] 5.1 Record the demotion rule where contributors see it (CONTRIBUTING.md CI section): any allow-failure/mute/retirement/trigger-narrowing/assertion-relaxation requires a tracked issue with an exit criterion; YAML comments do not count. +- [x] 5.2 End-to-end verification: one dispatch of ci.yml shows `address`, `undefined`, `thread`, `fuzz` green and no `memory` job; nightly (or dispatch) shows `dependency-scan` green and unmuted; grep of `.github/workflows/ci.yml` finds no `allow_failure`, no `|| true` in gate steps. + - PR run (8cb964d) green across address/undefined/thread/fuzz; dispatch 27316418663 Dependency Scan green; greps: `allow_failure` only in a comment, `|| true` only in non-gate contexts (header-guard grep fallback, packaging `ls`). +- [x] 5.3 Update audit-wiki Verification Lanes entity and CI-THESIS.md R1 status once lanes hold green, so the wiki reflects enforcement reality. From be2fb37eefb52b1ee9099aef1df0fbaf12faf3e1 Mon Sep 17 00:00:00 2001 From: Charles Hoskinson <Charles.Hoskinson@gmail.com> Date: Fri, 12 Jun 2026 11:01:52 -0600 Subject: [PATCH 11/15] openspec: archive ci-stabilize-mandatory-lanes, baseline ci-stabilization spec --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/ci-stabilization/spec.md | 0 .../tasks.md | 0 openspec/specs/ci-stabilization/spec.md | 76 ++++++++++++++++++- 6 files changed, 73 insertions(+), 3 deletions(-) rename openspec/changes/{ci-stabilize-mandatory-lanes => archive/2026-06-12-ci-stabilize-mandatory-lanes}/.openspec.yaml (100%) rename openspec/changes/{ci-stabilize-mandatory-lanes => archive/2026-06-12-ci-stabilize-mandatory-lanes}/design.md (100%) rename openspec/changes/{ci-stabilize-mandatory-lanes => archive/2026-06-12-ci-stabilize-mandatory-lanes}/proposal.md (100%) rename openspec/changes/{ci-stabilize-mandatory-lanes => archive/2026-06-12-ci-stabilize-mandatory-lanes}/specs/ci-stabilization/spec.md (100%) rename openspec/changes/{ci-stabilize-mandatory-lanes => archive/2026-06-12-ci-stabilize-mandatory-lanes}/tasks.md (100%) diff --git a/openspec/changes/ci-stabilize-mandatory-lanes/.openspec.yaml b/openspec/changes/archive/2026-06-12-ci-stabilize-mandatory-lanes/.openspec.yaml similarity index 100% rename from openspec/changes/ci-stabilize-mandatory-lanes/.openspec.yaml rename to openspec/changes/archive/2026-06-12-ci-stabilize-mandatory-lanes/.openspec.yaml diff --git a/openspec/changes/ci-stabilize-mandatory-lanes/design.md b/openspec/changes/archive/2026-06-12-ci-stabilize-mandatory-lanes/design.md similarity index 100% rename from openspec/changes/ci-stabilize-mandatory-lanes/design.md rename to openspec/changes/archive/2026-06-12-ci-stabilize-mandatory-lanes/design.md diff --git a/openspec/changes/ci-stabilize-mandatory-lanes/proposal.md b/openspec/changes/archive/2026-06-12-ci-stabilize-mandatory-lanes/proposal.md similarity index 100% rename from openspec/changes/ci-stabilize-mandatory-lanes/proposal.md rename to openspec/changes/archive/2026-06-12-ci-stabilize-mandatory-lanes/proposal.md diff --git a/openspec/changes/ci-stabilize-mandatory-lanes/specs/ci-stabilization/spec.md b/openspec/changes/archive/2026-06-12-ci-stabilize-mandatory-lanes/specs/ci-stabilization/spec.md similarity index 100% rename from openspec/changes/ci-stabilize-mandatory-lanes/specs/ci-stabilization/spec.md rename to openspec/changes/archive/2026-06-12-ci-stabilize-mandatory-lanes/specs/ci-stabilization/spec.md diff --git a/openspec/changes/ci-stabilize-mandatory-lanes/tasks.md b/openspec/changes/archive/2026-06-12-ci-stabilize-mandatory-lanes/tasks.md similarity index 100% rename from openspec/changes/ci-stabilize-mandatory-lanes/tasks.md rename to openspec/changes/archive/2026-06-12-ci-stabilize-mandatory-lanes/tasks.md diff --git a/openspec/specs/ci-stabilization/spec.md b/openspec/specs/ci-stabilization/spec.md index f6e2d48..e986aad 100644 --- a/openspec/specs/ci-stabilization/spec.md +++ b/openspec/specs/ci-stabilization/spec.md @@ -1,5 +1,9 @@ -## ADDED Requirements +## Purpose +Define the CI signal contract: which validation lanes are required for ordinary +pushes and pull requests, which are optional, and what each lane must guarantee +(deterministic save/load, repository hygiene, coverage reporting). +## Requirements ### Requirement: Primary CI Signal Normal source pushes and pull requests SHALL have one high-signal required validation set: Linux headless, Windows headless, ABI verification, coverage artifact generation, Sprint 2 checks, and Module DAG architecture checks. @@ -10,14 +14,19 @@ Normal source pushes and pull requests SHALL have one high-signal required valid - **AND** optional backend and research checks SHALL NOT duplicate the same headless failure as separate required failures ### Requirement: Optional Validation Lanes -PAL backends, SDL backends, macOS, sanitizers, fuzzing, TLA+, dependency scanning, and duplicate full Module DAG builds SHALL be clearly optional for ordinary pushes. +PAL backends, SDL backends, macOS, TLA+, and duplicate full Module DAG builds SHALL be clearly optional for ordinary pushes. Sanitizer lanes (ASan, UBSan, TSan), fuzz smoke runs, and dependency scanning SHALL NOT be classed as optional: sanitizers and fuzz gate at their pull-request/master tier, and dependency scanning gates at its nightly/dispatch tier. #### Scenario: Optional lanes - **GIVEN** ordinary source changes are pushed - **WHEN** optional validation is not explicitly requested -- **THEN** these lanes SHALL run only when path-gated, scheduled, manually dispatched, or tag-oriented +- **THEN** the optional lanes SHALL run only when path-gated, scheduled, manually dispatched, or tag-oriented - **AND** their job names SHALL identify them as optional +#### Scenario: Enforced lanes are not named optional +- **GIVEN** a lane gates at any trigger tier +- **WHEN** its job name is rendered in the Actions UI +- **THEN** the name SHALL NOT contain "Optional" + ### Requirement: Deterministic Save/Load Save/load and replay determinism SHALL preserve the hash-relevant CPU and lightweight context state. @@ -43,3 +52,64 @@ Coverage SHALL run independently from optional backend lanes and SHALL publish a - **WHEN** coverage is captured - **THEN** `coverage.filtered.info` SHALL be uploaded - **AND** the coverage threshold policy SHALL be explicitly documented as report-only until a baseline is established + +### Requirement: Mandatory Lanes Are Deterministically Green +The ASan, UBSan, and fuzz lanes in `.github/workflows/ci.yml` SHALL pass deterministically at their existing trigger tier. Red runs SHALL be resolved by fixing the root cause or by quarantining the affected test under an issue-linked `DISABLED_` marker; deleting or weakening an assertion to obtain green SHALL NOT be an accepted fix. + +#### Scenario: Clean change passes the sanitizer and fuzz lanes +- **GIVEN** a pull request that introduces no memory error, undefined behavior, or fuzz-reachable crash +- **WHEN** the `sanitizers` (address, undefined) and `fuzz` jobs run +- **THEN** both jobs SHALL conclude success + +#### Scenario: Quarantine preserves the record +- **WHEN** a failing test is quarantined instead of fixed +- **THEN** the test SHALL carry a `DISABLED_` marker referencing a tracked issue stating the exit criterion + +### Requirement: TSan Gates via Suppression File +A `tsan-suppressions.txt` SHALL be checked into the repository, containing one entry per known race, each annotated with its tracking issue. The TSan matrix entry SHALL load it via `TSAN_OPTIONS=suppressions=` and SHALL NOT carry `allow_failure`. The CI job SHALL install a symbolizer (`llvm-symbolizer`) so suppressions can match. The `tsan` CMake preset SHALL apply the same suppression file so local runs and CI agree on the known-race set. + +#### Scenario: New race fails the lane +- **GIVEN** a pull request introducing a data race not matched by `tsan-suppressions.txt` +- **WHEN** the `thread` sanitizer matrix entry runs +- **THEN** the job SHALL fail and the workflow conclusion SHALL be failure + +#### Scenario: Known race is suppressed and tracked +- **GIVEN** a race entry present in `tsan-suppressions.txt` +- **WHEN** the entry is read +- **THEN** it SHALL be preceded by a comment linking a tracked issue whose closure removes the entry + +#### Scenario: Local reproduction matches CI +- **WHEN** the `tsan` test preset runs locally +- **THEN** the same suppression file SHALL be in effect as in the CI `thread` matrix entry + +### Requirement: MSan Leg Retired with Re-entry Condition +The `memory` sanitizer matrix entry SHALL be removed from `.github/workflows/ci.yml`. A tracked issue SHALL record the retirement and its re-entry condition: an MSan-instrumented libc++ (and instrumented dependency surface), with any re-introduced lane placed at the nightly tier. No `msan` CMake preset SHALL be added while the lane is retired. + +#### Scenario: No MSan execution after retirement +- **WHEN** the `sanitizers` job matrix is expanded on any trigger +- **THEN** no `memory` entry SHALL be present + +#### Scenario: Re-entry is auditable +- **WHEN** the retirement lands +- **THEN** a tracked issue SHALL exist stating the instrumented-libc++ re-entry condition + +### Requirement: Dependency Scan Produces an Honest Verdict +The `dependency-scan` job SHALL invoke osv-scanner only in modes the tool supports (no unparseable `--lockfile` input), SHALL upload its findings as artifacts, and SHALL NOT mute failures via `|| true` or `continue-on-error` once the invocation is fixed and a triaged green dispatch run is recorded. + +#### Scenario: Vulnerability fails the scheduled run +- **GIVEN** the fixed invocation and a dependency with a known unsuppressed vulnerability +- **WHEN** the nightly or dispatched `dependency-scan` job runs +- **THEN** the job SHALL conclude failure and its findings SHALL be uploaded as an artifact + +#### Scenario: Unmute only after rehearsal +- **WHEN** the mutes are removed +- **THEN** a prior `workflow_dispatch` run of the fixed invocation SHALL have concluded success + +### Requirement: Lane Demotion Requires a Tracked Exit Criterion +No CI lane SHALL be demoted — marked allow-failure, muted, retired, narrowed in trigger tier, or relaxed in its assertions — without a tracked issue stating the demotion and the criterion for restoring enforcement. YAML comments SHALL NOT substitute for the tracked issue. + +#### Scenario: Demotion carries its exit +- **GIVEN** a change that sets `continue-on-error`/`allow_failure`, removes a lane, or narrows a lane's triggers +- **WHEN** the change is reviewed +- **THEN** it SHALL reference a tracked issue stating the exit criterion, and a change lacking one SHALL be rejected + From 9fa8bd3ba072b3074b1223b7c6eca9ec414f467d Mon Sep 17 00:00:00 2001 From: Charles Hoskinson <Charles.Hoskinson@gmail.com> Date: Fri, 12 Jun 2026 11:13:42 -0600 Subject: [PATCH 12/15] ci: pin windows runners to windows-2025 windows-2026 image (VS 18, MSVC 19.51) emits C4875 in gsl-lite under /WX. Rollout is gradual: green on windows-2025 at 03:00 UTC, red on windows-2026 at 17:02 UTC the same day. Unpin tracked separately. --- .github/workflows/ci.yml | 12 ++++++++---- .github/workflows/module-dag.yml | 3 ++- .github/workflows/pal-ci.yml | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49f2d86..7df3c89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,7 +188,9 @@ jobs: # ───────────────────────────────────────────────────────────────────────────── windows: name: Windows (MSVC) - runs-on: windows-latest + # windows-2025 pin: windows-2026 (VS 18 / MSVC 19.51) emits C4875 in gsl-lite + # under /WX. Unpin after a gsl-lite upgrade or VS18 flag handling goes green. + runs-on: windows-2025 timeout-minutes: 30 steps: @@ -224,7 +226,8 @@ jobs: # ───────────────────────────────────────────────────────────────────────────── windows-sdl3: name: Optional Windows SDL3 (MSVC) - runs-on: windows-latest + # windows-2025 pin: see windows job + runs-on: windows-2025 timeout-minutes: 30 if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') @@ -820,11 +823,12 @@ jobs: needs: [linux, windows, macos, linux-sdl3, windows-sdl3, macos-sdl3] strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-15] + # windows-2025 pin: see windows job + os: [ubuntu-latest, windows-2025, macos-15] include: - os: ubuntu-latest build_type: Release - - os: windows-latest + - os: windows-2025 build_type: Release - os: macos-15 build_type: Release diff --git a/.github/workflows/module-dag.yml b/.github/workflows/module-dag.yml index c531b32..6291c2b 100644 --- a/.github/workflows/module-dag.yml +++ b/.github/workflows/module-dag.yml @@ -155,7 +155,8 @@ jobs: # ───────────────────────────────────────────────────────────────────────────── build-windows: name: Optional Build (Windows) - runs-on: windows-latest + # windows-2025 pin: see ci.yml windows job + runs-on: windows-2025 needs: [include-rules, cmake-dag] if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' diff --git a/.github/workflows/pal-ci.yml b/.github/workflows/pal-ci.yml index 1452d57..b10e739 100644 --- a/.github/workflows/pal-ci.yml +++ b/.github/workflows/pal-ci.yml @@ -246,7 +246,8 @@ jobs: windows-build: name: Optional Windows Build - runs-on: windows-latest + # windows-2025 pin: see ci.yml windows job + runs-on: windows-2025 steps: - uses: actions/checkout@v4 From c889ac6c0acc7dec2e9d60a2e54dedc030e4eca1 Mon Sep 17 00:00:00 2001 From: Charles Hoskinson <Charles.Hoskinson@gmail.com> Date: Fri, 12 Jun 2026 11:22:27 -0600 Subject: [PATCH 13/15] Revert "ci: pin windows runners to windows-2025" This reverts commit 9fa8bd3ba072b3074b1223b7c6eca9ec414f467d. --- .github/workflows/ci.yml | 12 ++++-------- .github/workflows/module-dag.yml | 3 +-- .github/workflows/pal-ci.yml | 3 +-- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7df3c89..49f2d86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,9 +188,7 @@ jobs: # ───────────────────────────────────────────────────────────────────────────── windows: name: Windows (MSVC) - # windows-2025 pin: windows-2026 (VS 18 / MSVC 19.51) emits C4875 in gsl-lite - # under /WX. Unpin after a gsl-lite upgrade or VS18 flag handling goes green. - runs-on: windows-2025 + runs-on: windows-latest timeout-minutes: 30 steps: @@ -226,8 +224,7 @@ jobs: # ───────────────────────────────────────────────────────────────────────────── windows-sdl3: name: Optional Windows SDL3 (MSVC) - # windows-2025 pin: see windows job - runs-on: windows-2025 + runs-on: windows-latest timeout-minutes: 30 if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') @@ -823,12 +820,11 @@ jobs: needs: [linux, windows, macos, linux-sdl3, windows-sdl3, macos-sdl3] strategy: matrix: - # windows-2025 pin: see windows job - os: [ubuntu-latest, windows-2025, macos-15] + os: [ubuntu-latest, windows-latest, macos-15] include: - os: ubuntu-latest build_type: Release - - os: windows-2025 + - os: windows-latest build_type: Release - os: macos-15 build_type: Release diff --git a/.github/workflows/module-dag.yml b/.github/workflows/module-dag.yml index 6291c2b..c531b32 100644 --- a/.github/workflows/module-dag.yml +++ b/.github/workflows/module-dag.yml @@ -155,8 +155,7 @@ jobs: # ───────────────────────────────────────────────────────────────────────────── build-windows: name: Optional Build (Windows) - # windows-2025 pin: see ci.yml windows job - runs-on: windows-2025 + runs-on: windows-latest needs: [include-rules, cmake-dag] if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' diff --git a/.github/workflows/pal-ci.yml b/.github/workflows/pal-ci.yml index b10e739..1452d57 100644 --- a/.github/workflows/pal-ci.yml +++ b/.github/workflows/pal-ci.yml @@ -246,8 +246,7 @@ jobs: windows-build: name: Optional Windows Build - # windows-2025 pin: see ci.yml windows job - runs-on: windows-2025 + runs-on: windows-latest steps: - uses: actions/checkout@v4 From 7d0db3631c805291cbb154bafb6f762974b189a1 Mon Sep 17 00:00:00 2001 From: Charles Hoskinson <Charles.Hoskinson@gmail.com> Date: Fri, 12 Jun 2026 11:22:27 -0600 Subject: [PATCH 14/15] build: disable C4875 from gsl-lite headers under new MSVC VS 18 2026 (MSVC 19.51) reached both windows-latest and windows-2025 images in-place, so runner pinning cannot hold the toolchain; the prior pin is reverted. C4875 fires inside gsl-lite (gsl-lite.hpp:2218), not project code. --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index a555f16..8a91207 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,10 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") endif() elseif(MSVC) target_compile_options(legends_compile_options INTERFACE /W4 /permissive- /utf-8) + # MSVC 19.51+ (VS 18 2026) deprecates non-string-literal [[gsl::suppress]] + # args (C4875); the pinned gsl-lite still emits them (gsl-lite.hpp:2218), + # fatal under /WX. Drop /wd4875 when the gsl-lite pin advances past the fix. + target_compile_options(legends_compile_options INTERFACE /wd4875) target_compile_definitions(legends_compile_options INTERFACE _CRT_SECURE_NO_WARNINGS) # Security hardening (H3) target_link_options(legends_compile_options INTERFACE /GUARD:CF) From f7f419ae4e343d99058b0192ff1b260d8bb1191e Mon Sep 17 00:00:00 2001 From: Charles Hoskinson <Charles.Hoskinson@gmail.com> Date: Fri, 12 Jun 2026 11:24:35 -0600 Subject: [PATCH 15/15] build: link issue #44 in /wd4875 comment --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a91207..347c373 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,7 +97,7 @@ elseif(MSVC) target_compile_options(legends_compile_options INTERFACE /W4 /permissive- /utf-8) # MSVC 19.51+ (VS 18 2026) deprecates non-string-literal [[gsl::suppress]] # args (C4875); the pinned gsl-lite still emits them (gsl-lite.hpp:2218), - # fatal under /WX. Drop /wd4875 when the gsl-lite pin advances past the fix. + # fatal under /WX. Drop /wd4875 when the gsl-lite pin advances (issue #44). target_compile_options(legends_compile_options INTERFACE /wd4875) target_compile_definitions(legends_compile_options INTERFACE _CRT_SECURE_NO_WARNINGS) # Security hardening (H3)