Skip to content

ci: link the MSan lane against an MSan-instrumented libc++ - #568

Open
Krilliac wants to merge 1 commit into
Workingfrom
claude/ci-msan-instrumented-libcxx
Open

ci: link the MSan lane against an MSan-instrumented libc++#568
Krilliac wants to merge 1 commit into
Workingfrom
claude/ci-msan-instrumented-libcxx

Conversation

@Krilliac

@Krilliac Krilliac commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Summary

Makes the build-linux-msan lane capable of producing meaningful MemorySanitizer reports. Until now it compiled the engine with -fsanitize=memory but linked the distribution's libc++, which is not instrumented; every std::string written inside libc++.so.1 then read as uninitialized. On the last run before this change that produced 732 reports within the first 25 tests, the 16 MiB sanitizer log hit the wrapper's file-size limit, SparkTests died of SIGXFSZ with no completion evidence, and the lane had been red on Working for months while costing 18 to 26 runner minutes per push.

What changes

  • Instrumented runtime. The job downloads the LLVM 18.1.3 source release that matches the runner's clang-18 (size, SHA-256 and SHA-512 pinned in the workflow), builds libc++ and libc++abi with LLVM_USE_SANITIZER=MemoryWithOrigins from the checked-in recipe .github/msan/libcxx-runtime.cmake, installs them under runner.temp, and caches that prefix with the split actions/cache/restore and save actions keyed by the recipe hash, the LLVM tag and the exact compiler package version. The prefix is verified on every run, hit or miss, before it is used or saved.
  • Consumer wiring. SparkTests is compiled with -nostdinc++ -isystem <prefix>/include/c++/v1 and linked with -L<prefix>/lib -Wl,-rpath,<prefix>/lib -lc++abi on both executable and shared-library link lines; libc++-dev is no longer installed, so a lost flag fails the link instead of silently linking the distro library. A verification step walks every ELF under build/bin and build/lib (SparkTests, probe executables, the dlopen'd fixtures and plugins, the engine and game-module shared libraries) and checks which libc++ they resolve, that the RUNPATH names the prefix, that libc++abi.so.1 is a direct dependency, and that each is MSan-linked.
  • Behavioural proof. Tests/TestMSanCanary.cpp adds two tests: one proves this translation unit is instrumented and not ignorelisted, the other proves that stores performed by out-of-line libc++ code update shadow memory. Against an uninstrumented runtime the second test is designed to fail; in non-MSan builds both are count-preserving stubs.
  • Never zero evidence again. The shared sanitizer wrapper ignores SIGXFSZ, so an over-cap write fails with EFBIG and truncates the log instead of killing the suite; a harness fixture covers it. The lane runs with halt_on_error=1, so a run yields one deduplicated signature at a time, and a new step labels pre-test infrastructure failures (fetch, runtime build, link verification) so they are never mistaken for findings.
  • Boundaries. ENABLE_VULKAN=OFF in this lane (the loader and lavapipe are uninstrumented; the Vulkan test is already a stub without a device). SDL2 was already off. The ignorelist loses dead patterns and the /usr/* entry that would have blinded every inlined std:: function once the headers moved; in-tree third-party entries stay for the proving phase.
  • Contracts and docs. Build-matrix parity expects the extra configure record, inventory regenerated last, readiness status and the reproducible-build recipe updated, both copies of the build/CI and validation skills updated. The lane stays continue-on-error and outside the required gate until a run classifies clean.

Verification

Static and repository validators only. MemorySanitizer cannot run on the Windows machine this was authored on, and WSL is not installed there, so the two Linux-only RED proofs (the SIGXFSZ fixture and the canary against an uninstrumented runtime) are left to CI.

Check Result
test-workflow-failure-propagation.py, test-verify-exact-required-gate.py, test_workflow_privilege_boundaries.py, test-verify-sanitizer-evidence.py OK
bash .github/scripts/test-run-sanitizer-tests.sh 118 passed, 0 failed, 5 skipped (the new flood case skips on Git Bash)
python Tests/Tools/test_build_matrix_parity.py 181 tests OK
Tools/buildmatrix/inventory.py --check, check_parity.py against the checked-in baseline exit 0, byte-identical
tools/docs_contract.py validate, tools/publish-wiki.py --check, tools/site-data/validate.py, tools/check-test-registration.sh OK
clang-format 18 on Tests/TestMSanCanary.cpp clean

Designed by a four-reader reconnaissance (job and sanitizer tooling, repository contracts, upstream MSan/libc++ facts from the 18.1.3 sources, engine library boundaries), reviewed by three adversarial critics whose objections were all accepted, then implemented and re-verified by three reviewers. The second review caught a blocker inherited from the plan: executables link the MSan runtime whole-archive and define __msan_* symbols rather than importing them, so the ELF check now distinguishes executables (by PT_INTERP) from shared objects. The plan's wrapper comment also contained an apostrophe inside a single-quoted script, which the harness itself exposed.

What only the CI run can prove

  • The tarball pins match the live release (a mismatch fails before any compile), the runtime builds inside the job budget, and the cache round-trips on the second run.
  • The ELF verification passes on real binaries, both canary tests pass against the instrumented prefix, and the suite completes with a classification of clean or sanitizer-finding rather than crash or verification-failure.
  • What the first instrumented run reports. The 732 libc++ string reports should vanish; whatever remains is triaged by origin: engine-origin uninitialized reads are bugs to fix, uninstrumented-library origins get an unpoison at the boundary, and anything inside libc++ itself means the runtime build is wrong.

Do not merge until the first run's report has been read and the lane classifies clean.

🤖 Generated with Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-06T14:21:37.929248Z 633b0ee PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

🔍 CodeQL Report

Languages: actions, c\-cpp, python
Errors: 0 | Warnings: 2 | Notes: 0

⚠️ Warnings (2)
Rule Languages Location Message
cpp/integer\-multiplication\-cast\-to\-long c-cpp ThirdParty/Utils/json/nlohmann\_json\.h:629 Multiplication result may overflow 'int' before it is converted to 'size_type'.
cpp/integer\-multiplication\-cast\-to\-long c-cpp ThirdParty/Utils/json/nlohmann\_json\.h:630 Multiplication result may overflow 'int' before it is converted to 'size_type'.

Analyzed commit: 8a0cfc0b23302519d71a8d8f3987a6dce4c72451

Updated: 2026-09-07T00:37:48.032Z — this comment is updated in-place.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Code Coverage (GCC + lcov)

ServiceInterfaces.h                            | 200%     3| 0.0%   3|    -    0
Version.h                                      | 150%     2| 0.0%   1|    -    0

[/home/runner/work/SparkEngine/SparkEngine/SparkServer/src/]
ServerApplication.cpp                          |11.7%   222| 0.0%  11|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/Blank3D/Source/]
GameModule.h                                   |20.5%    83| 0.0%  14|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/EmptyProject/Source/]
GameModule.h                                   |26.7%    60| 0.0%  13|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/FPSStarter/Source/]
GameModule.h                                   |14.9%   383| 0.0%  48|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/MMOStarter/Source/]
GameModule.h                                   |12.7%   237| 0.0%  26|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/MultiplayerArena/Source/]
GameModule.h                                   |16.9%   183| 0.0%  25|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/PlatformerKit/Source/]
GameModule.h                                   |12.2%   245| 0.0%  28|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/RPGStarter/Source/]
GameModule.h                                   |11.5%   338| 0.0%  37|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/ThirdPersonStarter/Source/]
GameModule.h                                   |10.6%   226| 0.0%  21|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/TopDownStarter/Source/]
GameModule.h                                   |10.8%   223| 0.0%  20|    -    0
================================================================================
                                         Total:|24.6% 69366| 0.0% 11k|    -    0

Per-Subsystem Coverage

Subsystem Lines Hit Coverage Threshold Status
AI 3809 1439 37.8% 35%
Animation 1966 861 43.8% 35%
Audio 1956 942 48.2% 30%
Camera 317 317 100% 40%
Core 10311 6357 61.7% 40%
ECS 1089 537 49.3% 40%
Editor 21389 9896 46.3% 25%
GameModules 16353 10252 62.7% 30%
Graphics 23464 10460 44.6% 30%
Networking 5199 3718 71.5% 35%
Physics 3468 1277 36.8% 35%
Scripting 1625 930 57.2% 30%
Utils 12655 8585 67.8% 60%

Selected-subsystem coverage: 53.6% (55571/103601 lines)

LCOV corpus coverage: 54.1% (69366/128180 lines); 24579 instrumented lines are outside the threshold table.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

❌ CI Error Report

Failed jobs: linux-asan, linux-tsan, windows-vs2022, aggregate-test-stats
Advisory lanes (continue-on-error): linux-msan — findings are informational and do not fail this report
Errors: 4 | Test failures: 7 | Compiler warnings: 25

Build Errors

Other errors (4)
effective_exit_code=1 [linux-asan, linux-msan, linux-tsan]
==16838==WARNING: MemorySanitizer: use-of-uninitialized-value [linux-msan]
SUMMARY: MemorySanitizer: use-of-uninitialized-value build/../SparkEngine/Source/Engine/Networking/NetworkReplication.cpp:80:9 in Spark::Net::NetworkManager::RegisterReplicatedEntity(Spark::Net::ReplicatedEntity const&) [linux-msan]
Required job 'aggregate-test-stats' concluded 'failure' without a diagnostic error-summary artifact. [aggregate-test-stats]

Test Failures

Test Jobs
[ FAILED ] EngineWiring_TriggerVolumeComponentPublishesEnterEventFromLifecycleTick (19.5ms, 5 assertions) linux-asan
[00:54:12.375] [TID:139996630314432] [ERROR] [Animation ] Failed to load skeleton '/tmp/spark_security_parsers_tests/... linux-asan
[CRASH] Assert triggered but crash handling disabled: ===== ASSERTION FAILED ===== linux-asan, linux-tsan
[00:41:38.457] [TID:139982276169216] [ERROR] [Animation ] Failed to load animations from '/tmp/spark_security_parsers... linux-msan
[CRASH] Assert crash behavior changed to: DISABLED linux-msan
[ FAILED ] EngineWiring_TriggerVolumeComponentPublishesEnterEventFromLifecycleTick (30.5ms, 5 assertions) linux-tsan
[00:32:06.219] [TID:139881079395584] [ERROR] [Animation ] Failed to load skeleton '/tmp/spark_security_parsers_tests/... linux-tsan
Compiler Warnings (25)
../../SparkAssetPipelineCore/src/AssetCooker.cpp:896:62: warning: ‘std::filesystem::__cxx11::path std::filesystem::__cxx11::u8path(const _Source&) [with _Source = std::__cxx11::basic_string<char>; _Require = path; _CharT = char]’ is deprecated: use 'path((const char8_t*)&*source)' instead [-Wdeprecated-declarations] [linux-asan, linux-tsan]
../../SparkAssetPipelineCore/tests/AssetCookerTests.cpp:178:55: warning: ‘std::filesystem::__cxx11::path std::filesystem::__cxx11::u8path(const _Source&) [with _Source = char8_t [3]; _Require = path; _CharT = char8_t]’ is deprecated: use 'path((const char8_t*)&*source)' instead [-Wdeprecated-declarations] [linux-asan, linux-tsan]
../../SparkAssetPipelineCore/tests/AssetCookerTests.cpp:179:69: warning: ‘std::filesystem::__cxx11::path std::filesystem::__cxx11::u8path(const _Source&) [with _Source = char8_t [8]; _Require = path; _CharT = char8_t]’ is deprecated: use 'path((const char8_t*)&*source)' instead [-Wdeprecated-declarations] [linux-asan, linux-tsan]
../../SparkAssetPipelineCore/tests/AssetCookerTests.cpp:182:58: warning: ‘std::filesystem::__cxx11::path std::filesystem::__cxx11::u8path(const _Source&) [with _Source = char8_t [7]; _Require = path; _CharT = char8_t]’ is deprecated: use 'path((const char8_t*)&*source)' instead [-Wdeprecated-declarations] [linux-asan, linux-tsan]
../../SparkCooker/src/main.cpp:171:63: warning: ‘std::filesystem::__cxx11::path std::filesystem::__cxx11::u8path(const _Source&) [with _Source = std::__cxx11::basic_string<char>; _Require = path; _CharT = char]’ is deprecated: use 'path((const char8_t*)&*source)' instead [-Wdeprecated-declarations] [linux-asan, linux-tsan]
../SparkEngine/Source/Core/ModuleManager.cpp:79:39: warning: ‘std::filesystem::__cxx11::path std::filesystem::__cxx11::u8path(_InputIterator, _InputIterator) [with _InputIterator = const char*; _Require = path; _CharT = char]’ is deprecated: use 'path(u8string(first, last))' instead [-Wdeprecated-declarations] [linux-asan, linux-tsan]
../SparkEngine/Source/Core/StartupSplash.cpp:95:51: warning: ‘std::filesystem::__cxx11::path std::filesystem::__cxx11::u8path(const _Source&) [with _Source = std::__cxx11::basic_string<char>; _Require = path; _CharT = char]’ is deprecated: use 'path((const char8_t*)&*source)' instead [-Wdeprecated-declarations] [linux-asan, linux-tsan]
../SparkEngine/Source/Core/StartupSplash.cpp:99:51: warning: ‘std::filesystem::__cxx11::path std::filesystem::__cxx11::u8path(const _Source&) [with _Source = const char*; _Require = path; _CharT = char]’ is deprecated: use 'path((const char8_t*)&*source)' instead [-Wdeprecated-declarations] [linux-asan, linux-tsan]
../../../GameModules/SparkGameMMOFPS/Source/World/TFAssetPaths.h:20:77: warning: ‘std::filesystem::__cxx11::path std::filesystem::__cxx11::u8path(const _Source&) [with _Source = std::__cxx11::basic_string<char>; _Require = path; _CharT = char]’ is deprecated: use 'path((const char8_t*)&*source)' instead [-Wdeprecated-declarations] [linux-asan, linux-tsan]
../SparkEngine/Source/Engine/SaveSystem/SaveSystem.cpp:665:36: warning: possibly dangling reference to a temporary [-Wdangling-reference] [linux-asan, linux-tsan]
../SparkEngine/Source/Graphics/GLTFStaticMeshLoader.cpp:70:73: warning: ‘std::filesystem::__cxx11::path std::filesystem::__cxx11::u8path(const _Source&) [with _Source = const char*; _Require = path; _CharT = char]’ is deprecated: use 'path((const char8_t*)&*source)' instead [-Wdeprecated-declarations] [linux-asan, linux-tsan]
../SparkEngine/Source/SceneManager/ReflectedSceneSerializer.cpp:294:48: warning: ‘std::filesystem::__cxx11::path std::filesystem::__cxx11::u8path(const _Source&) [with _Source = std::__cxx11::basic_string<char>; _Require = path; _CharT = char]’ is deprecated: use 'path((const char8_t*)&*source)' instead [-Wdeprecated-declarations] [linux-asan, linux-tsan]
clang++: warning: argument unused during compilation: '-stdlib=libc++' [-Wunused-command-line-argument] [linux-msan]
../../SparkAssetPipelineCore/src/AssetCooker.cpp:896:56: warning: 'u8path<std::string, 0>' is deprecated [-Wdeprecated-declarations] [linux-msan]
../../SparkCooker/src/main.cpp:171:57: warning: 'u8path<std::string, 0>' is deprecated [-Wdeprecated-declarations] [linux-msan]
../SparkEngine/Source/Core/ModuleManager.cpp:79:33: warning: 'u8path<const char *, 0>' is deprecated [-Wdeprecated-declarations] [linux-msan]
../SparkEngine/Source/Core/StartupSplash.cpp:95:45: warning: 'u8path<std::string, 0>' is deprecated [-Wdeprecated-declarations] [linux-msan]
../SparkEngine/Source/Core/StartupSplash.cpp:99:45: warning: 'u8path<const char *, 0>' is deprecated [-Wdeprecated-declarations] [linux-msan]
../SparkEngine/Source/Graphics/GLTFStaticMeshLoader.cpp:70:67: warning: 'u8path<const char *, 0>' is deprecated [-Wdeprecated-declarations] [linux-msan]
../SparkEngine/Source/SceneManager/ReflectedSceneSerializer.cpp:294:42: warning: 'u8path<std::string, 0>' is deprecated [-Wdeprecated-declarations] [linux-msan]
... and 5 more

Updated: 2026-09-07T01:25:01Z — this comment is updated in-place, not duplicated.

build-linux-msan compiled the engine with -fsanitize=memory but linked
the distribution's libc++, which is not instrumented, so every
std::string written inside libc++.so.1 read as uninitialized: 732
reports in the first 25 tests, a 16 MiB sanitizer log that hit the
wrapper's file-size limit, SparkTests killed by SIGXFSZ with no
completion evidence, and a lane red on Working for months at 18-26
runner minutes per push.

The job now fetches the LLVM 18.1.3 source release matching the runner's
clang-18 (size, SHA-256 and SHA-512 pinned), builds libc++/libc++abi
with LLVM_USE_SANITIZER=MemoryWithOrigins from the checked-in recipe
.github/msan/libcxx-runtime.cmake, caches the install prefix through
split actions/cache restore/save keyed by recipe hash, LLVM tag and
compiler package version, and verifies the prefix on every run before it
is used or saved. SparkTests is compiled with -nostdinc++/-isystem and
linked with -L/-rpath/-lc++abi against that prefix; libc++-dev is no
longer installed so a lost flag fails the link. A verification step
walks every ELF under build/bin and build/lib and checks which libc++
it resolves, its RUNPATH, its direct libc++abi dependency and that it
is MSan-linked (executables define the runtime symbols, shared objects
import them). Tests/TestMSanCanary.cpp proves at run time that this
translation unit is instrumented and that libc++ stores update shadow
memory.

The shared sanitizer wrapper ignores SIGXFSZ so an over-cap write
truncates the log instead of killing the suite (harness fixture added);
the lane runs with halt_on_error=1 so each run yields one deduplicated
signature; a new step labels pre-test infrastructure failures so they
are never mistaken for findings; ENABLE_VULKAN=OFF removes the last
uninstrumented library the tests could reach; the ignorelist loses dead
patterns and the /usr/* entry. Parity expects the extra configure
record, inventory regenerated last, readiness status, the reproducible
recipe and both copies of the build/CI and validation skills updated.
The lane stays continue-on-error and outside the required gate until a
run classifies clean.

Static and repository validators only (workflow shape, exact gate,
privilege boundaries, sanitizer evidence and pipeline harness, parity,
inventory, docs contract, wiki, site data, registration): all pass. The
SIGXFSZ fixture and the canary's RED side need Linux; the first CI run
of this branch is their proof.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Krilliac
Krilliac force-pushed the claude/ci-msan-instrumented-libcxx branch from 633b0ee to 8a0cfc0 Compare September 6, 2026 23:22
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Code Coverage (GCC + lcov)

ServiceInterfaces.h                            | 200%     3| 0.0%   3|    -    0
Version.h                                      | 150%     2| 0.0%   1|    -    0

[/home/runner/work/SparkEngine/SparkEngine/SparkServer/src/]
ServerApplication.cpp                          |11.7%   222| 0.0%  11|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/Blank3D/Source/]
GameModule.h                                   |20.5%    83| 0.0%  14|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/EmptyProject/Source/]
GameModule.h                                   |26.7%    60| 0.0%  13|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/FPSStarter/Source/]
GameModule.h                                   |14.9%   383| 0.0%  48|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/MMOStarter/Source/]
GameModule.h                                   |12.7%   237| 0.0%  26|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/MultiplayerArena/Source/]
GameModule.h                                   |16.9%   183| 0.0%  25|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/PlatformerKit/Source/]
GameModule.h                                   |12.2%   245| 0.0%  28|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/RPGStarter/Source/]
GameModule.h                                   |11.5%   338| 0.0%  37|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/ThirdPersonStarter/Source/]
GameModule.h                                   |10.6%   226| 0.0%  21|    -    0

[/home/runner/work/SparkEngine/SparkEngine/Templates/TopDownStarter/Source/]
GameModule.h                                   |10.8%   223| 0.0%  20|    -    0
================================================================================
                                         Total:|24.6% 69359| 0.0% 11k|    -    0

Per-Subsystem Coverage

Subsystem Lines Hit Coverage Threshold Status
AI 3809 1439 37.8% 35%
Animation 1966 861 43.8% 35%
Audio 1956 942 48.2% 30%
Camera 317 317 100% 40%
Core 10311 6357 61.7% 40%
ECS 1089 537 49.3% 40%
Editor 21389 9896 46.3% 25%
GameModules 16353 10244 62.6% 30%
Graphics 23464 10460 44.6% 30%
Networking 5199 3718 71.5% 35%
Physics 3468 1277 36.8% 35%
Scripting 1625 930 57.2% 30%
Utils 12655 8585 67.8% 60%

Selected-subsystem coverage: 53.6% (55563/103601 lines)

LCOV corpus coverage: 54.1% (69359/128180 lines); 24579 instrumented lines are outside the threshold table.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant