ci: fail the build when an executable embeds a standard library header path - #7773
Conversation
gsl::not_null defaults a nostd::source_location argument in its constructor (src/gsl/pointers.h), so constructing one through a forwarding wrapper - std::stack::emplace(), std::optional::emplace(), std::make_unique() - captures the wrapper's own header instead of the call site. Nothing catches that today. It costs us twice. gsl::details::terminate() prints the captured location (src/gsl/assert.cpp), so a failed precondition names a standard library internal rather than the code that broke it. And the recorded path belongs to the toolchain, so on darwin it comes from the macOS SDK, whose location varies between builders and makes the release binaries irreproducible. 966f532 did exactly this in evo/creditpool.cpp and it went unnoticed until guix attestation for v24.0.0-rc.1 disagreed on the macOS hashes - six days after the commit landed, and only because one builder kept their SDK somewhere other than the rest. Comparing hashes across builders is a poor detector: it needs a second builder, a full release build, and a difference in their setups to fire at all. Add a check that fires on a single build instead, on any host. Verified against the real artifacts: the pre-fix build of 0f87636 fails on dashd, dash-qt and test_dash, which are precisely the three binaries that differed between builders, and passes on dash-cli, dash-tx, dash-util and dash-wallet, which are precisely the four that were byte-identical. Only the sections that hold string literals are examined. DWARF names standard library headers legitimately, and skipping debug sections by name is not portable: PE stores a long section name as an offset into the string table, so they read as '/81' rather than '.debug_*' and a denylist lets the whole DWARF include-directory table through. Naming the sections we want cannot pick up debug data by accident - an object file carrying twelve stdlib paths reports only the one in __TEXT,__cstring. Each hit is reported with its section and offset, so a real literal is distinguishable at a glance and can be attributed to referencing code without a rebuild. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guix build is the wrong place to learn that a source location was captured inside a standard library header: it runs on tags and on labelled pull requests, so a regression can sit on develop for weeks. Nothing about the check needs guix. Unlike check-symbols and check-security, which assert release-artifact properties - the glibc floor, the hardening flags - that a native CI build legitimately violates, an embedded standard library header path is wrong in any build on any host. Run it from build_src.sh behind RUN_STDLIB_PATH_CHECK, enabled for linux64 and mac so both object formats and both standard libraries are covered. It stays off by default rather than on. Sanitizer and fuzz builds record source locations for their own diagnostics and are expected to name standard library headers legitimately, so enabling it there would report instrumentation as a defect. Those targets can be revisited once we know what they actually embed. Note that these builds run from a distdir, so the script has to be listed in EXTRA_DIST alongside the other binary checks, which the preceding commit does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
✅ Final review complete — no blockers (commit b6b9b75) · triage: normal |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (8)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review. WalkthroughThe change adds a Python checker that scans selected binary sections for printable paths containing Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant CI as ci/dash/build_src.sh
participant Guix as contrib/guix/libexec/build.sh
participant Make as src Makefile
participant Checker as stdlib-path-check.py
participant Binaries as bin_PROGRAMS
CI->>Make: Run check-stdlib-paths when RUN_STDLIB_PATH_CHECK is true
Guix->>Make: Run check-stdlib-paths after security and symbol checks
Make->>Checker: Pass bin_PROGRAMS
Checker->>Binaries: Scan selected literal sections
Checker-->>Make: Return status for matches or parse errors
Merge Risk: ⚪ Minimal · up to This adds a build-time check for embedded C++ standard-library header paths. It does not change runtime behavior. No merge-blocking risk was found in the supplied context. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The new check is confined to build-time validation and does not modify the executables it examines. Failures stop the inspected build scripts. No introduced security defect was established, but build-runner privileges, isolation, and external publication behavior remain incompletely verified. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 6 files. (2 skipped: 2 unsupported.)
✨ Finishing Touches 💡 1🧪 Generate unit tests (beta)
🛠️ Fix failing CI checks 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
The checker, source-distribution wiring, and Guix/CI invocation are consistent with the PR's stated goal. One non-blocking coverage gap is confirmed: the regex misses Debian/Ubuntu multiarch standard-library header paths; no supplied finding establishes a blocking defect.
🟡 1 suggestion(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: dash-core-commit-history); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
normalbygpt-6.1-sol(effort low) — The change introduces executable section parsing and path detection across Guix and native CI build gates, requiring ordinary tooling review but changing no critical runtime surface. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— dash-core-commit-history (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 13% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— dash-core-commit-history (completed, effort high); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `contrib/guix/stdlib-path-check.py`:
- [SUGGESTION] contrib/guix/stdlib-path-check.py:40-42: Regex misses Debian multiarch stdlib header paths
The pattern requires the contiguous substring `include/c++/`, so it misses libstdc++ headers installed under Debian/Ubuntu's `include/<triplet>/c++/<version>/` layout. Testing the exact regex from this file confirms that `/usr/include/c++/13/bits/stl_construct.h` matches, while `/usr/include/x86_64-linux-gnu/c++/13/bits/c++config.h` does not. An embedded literal naming a header in that directory would therefore pass the new gate, despite being a standard-library header path. This does not undermine detection of the forwarding-wrapper regressions described in the PR, whose headers use the main include directory, so this is non-blocking. Allow intervening directory components while retaining the existing printable-byte restriction.
| # Matches the include directory of both libc++ (include/c++/v1) and libstdc++ | ||
| # (include/c++/<version>), wherever the toolchain or sysroot places it. | ||
| STDLIB_INCLUDE = re.compile(rb'[\x20-\x7e]*include/c\+\+/[\x20-\x7e]*') |
There was a problem hiding this comment.
🟡 Suggestion: Regex misses Debian multiarch stdlib header paths
The pattern requires the contiguous substring include/c++/, so it misses libstdc++ headers installed under Debian/Ubuntu's include/<triplet>/c++/<version>/ layout. Testing the exact regex from this file confirms that /usr/include/c++/13/bits/stl_construct.h matches, while /usr/include/x86_64-linux-gnu/c++/13/bits/c++config.h does not. An embedded literal naming a header in that directory would therefore pass the new gate, despite being a standard-library header path. This does not undermine detection of the forwarding-wrapper regressions described in the PR, whose headers use the main include directory, so this is non-blocking. Allow intervening directory components while retaining the existing printable-byte restriction.
| # Matches the include directory of both libc++ (include/c++/v1) and libstdc++ | |
| # (include/c++/<version>), wherever the toolchain or sysroot places it. | |
| STDLIB_INCLUDE = re.compile(rb'[\x20-\x7e]*include/c\+\+/[\x20-\x7e]*') | |
| # Matches the include directory of both libc++ (include/c++/v1) and libstdc++ | |
| # (include/c++/<version>), including multiarch layouts such as | |
| # include/<triplet>/c++/<version>, wherever the toolchain or sysroot places it. | |
| STDLIB_INCLUDE = re.compile(rb'[\x20-\x7e]*include/(?:[-\w.]+/)*c\+\+/[\x20-\x7e]*') |
source: muse-spark-1.3-contributor (phase1-reviewer: general)
c41035f guix: mount the macOS SDK at a fixed path in the container (UdjinM6) Pull request description: ## Issue being fixed or feature implemented Every container share whose value reaches the compiler is mapped to a fixed path. `contrib/guix/guix-build` shares `$PWD` as `/dash` and `DISTSRC_BASE` as `/distsrc-base` precisely so the build host's layout cannot end up in the outputs. `SDK_PATH` was the exception: it was shared with no target, so the SDK sat at the builder's own host path inside the container, and depends hands that path to the compiler as `-isysroot$(OSX_SDK)` (`depends/hosts/darwin.mk:7,74-81`). What still records that path is the debug output. `dsymutil` writes the include directory of every header it collected, so the darwin `*-debug.tar.gz` carries the builder's SDK path tens of thousands of times over - 37,337 occurrences measured on a current build - and two builders who keep their SDK in different places do not agree on that archive. This is worth being precise about, because it is easy to overstate. **It does not affect an attested artifact.** `contrib/guix/guix-attest` filters `apple-darwin-debug.tar` out of both `noncodesigned.SHA256SUMS` (`:193-199`) and `all.SHA256SUMS` (`:220-228`), and `doc/release-process.md:202-214` does not ship it. So nothing a release verifies depends on the SDK's location. This is reproducibility hygiene for a developer artifact, plus closing the one compiler-facing share that was not normalised. The separate and genuinely release-affecting problem - a `source_location` captured inside a standard library header, which put the SDK path into the shipped binaries as a string literal and produced the differing macOS hashes on v24.0.0-rc.1 - is fixed in #7772 and detected from now on by #7773. That defect is not what this PR addresses, and this PR is not needed to fix it. ## What was done? Resolve the SDK's parent directory host-side, with `make print-SDK_PATH`, inside the loop that already verifies the SDK exists, and mount it at a fixed `/macos-sdk`, passing `SDK_PATH=/macos-sdk` into the container. Resolving the path rather than testing whether `SDK_PATH` is set is what makes the default and an explicit setting converge. `contrib/containers/guix/scripts/guix-start:13` exports `SDK_PATH` as `"${WORKSPACE_PATH}/depends/SDKs"`, so the value already differed between builders who use the containerised workflow from different directories. Normalising only the explicitly-set case would have left two hash classes and fixed nothing. The share is made whenever `HOSTS` contains a darwin host, for every host's container in that run, which is what it did before this mapping existed - only darwin's depends reads `SDK_PATH`, so the others ignore it. A build with no darwin host now shares nothing even if `SDK_PATH` happens to be exported, which was not guaranteed before. `guix-clean` and the precious-directory logic keep operating on the host-side path and are unaffected. `guix-codesign` does not build depends and never consumed `SDK_PATH`. This diverges from Bitcoin Core, which carries the same un-remapped `${SDK_PATH:+--share="$SDK_PATH"}` line. The divergence is deliberate rather than a missed backport, and is confined to guix-build's existing SDK check and share. ## How Has This Been Tested? A local guix build with `SDK_PATH=/tmp/macOS-SDKs` and a CI guix build with no custom `SDK_PATH` produced identical output for the same commit, which is the property the change exists to provide: the two cases previously resolved to different container paths. Measured before the change, on a green build of the current tree: the shipped `...-arm64-apple-darwin-debug.tar.gz` contains 37,337 occurrences of the builder's SDK path, as DWARF include-directory entries. A control on the same scan found 44,348 `/dash/depends` paths, confirming the pipeline was reading the archive rather than silently returning nothing. The gating was exercised across five combinations - linux-only with `SDK_PATH` unset and set, mixed linux plus darwin with it unset and set, and darwin-only - confirming that both darwin cases converge on `/macos-sdk` and that a linux-only build shares nothing. ## Breaking Changes None to any shipped or attested artifact. The darwin `*-debug.tar.gz` and its per-host `SHA256SUMS.part` line change for every builder, including those using the default `SDK_PATH`, because the recorded path moves from `<workspace>/depends/SDKs/Xcode-...` to `/macos-sdk/Xcode-...`. Debug archives built before and after this change are therefore not comparable. Best landed between release cycles rather than during an active tagged RC's signing and verification. ## Checklist: - [x] I have performed a self-review of my own code - [x] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone 🤖 Generated with [Claude Code](https://claude.com/claude-code) Top commit has no ACKs. Tree-SHA512: f0c3111f734173b94dab13fdda15ceaef6682936db29a71939e76b2d84960912cffbb0a3052cfd5ae19855c226996def39c1a64e0c09849ac6597a62088d4046
Issue being fixed or feature implemented
A source location captured inside a standard library header is wrong twice over:
gsl::details::terminate()prints it, so a failed precondition names a standard library internal instead of the code that broke it, and the recorded path belongs to the toolchain, so on darwin it comes from the macOS SDK - whose location differs between builders and makes the release binaries irreproducible.Nothing catches that today. 966f532 introduced exactly this bug in
evo/creditpool.cppand it went unnoticed until guix attestation for v24.0.0-rc.1 disagreed on the macOS hashes: just over seven days after the commit landed, and only because one builder happened to keep their SDK somewhere other than the rest. Comparing hashes across builders is a poor detector. It needs a second builder, a full release build, and a difference in their setups before it fires at all, and it fires at the worst possible moment.What was done?
A check that fires on a single build instead.
contrib/guix/stdlib-path-check.pyparses each executable with LIEF and fails if one embeds a C++ standard library header path, wired as acheck-stdlib-pathsmake target beside the existingcheck-symbolsandcheck-security, and invoked from the guix build.It also runs in regular CI, behind
RUN_STDLIB_PATH_CHECK, enabled for thelinux64andmactargets.check-symbolscould not be run there - it caps GLIBC at 2.31 (contrib/guix/symbol-check.py:31-38) while a native CI build links against the container's far newer glibc, so it asserts a property of release artifacts that a native build legitimately violates - andcheck-securityis simply not wired into regular CI today. This check has no such dependence: outside the instrumented builds noted below, an embedded standard library header path is wrong regardless of host or toolchain. Regular CI builds from amake distdirtree (ci/dash/build_src.sh:29-37), so the script is listed inBIN_CHECKS/EXTRA_DIST.Both targets matter, and neither is redundant. On libc++ the relocation form of this bug is invisible, because clang proves the moved-from pointer non-null and drops the branch that keeps the string alive; gcc does not. On libstdc++ it is visible. Conversely the original rc.1 form - a raw pointer forwarded into the standard library - is visible on both. So darwin alone cannot cover this class, and
linux64should not be dropped on the assumption that it does.Only sections that hold string literals are examined:
.rodata,.rdata,__cstring,__const, and anything with those prefixes, so ELF variants such as.rodata.str1.1are included. DWARF legitimately names standard library headers for inlined template code, and skipping debug sections by name is not portable - PE stores a long section name as an offset into the string table, so they read as/81rather than.debug_*. A first version denied those by name and drowned the mingw build in thousands of DWARF false positives. Naming the sections we want cannot pick up debug data by accident. The trade is deliberate: a literal in an unusual section would be missed, which for a gate that blocks builds is the safer direction.Each hit reports its section, how many times the path occurs, and the first few virtual addresses, so a real literal is distinguishable at a glance from debug data, and can be attributed to the referencing code with
objdumpwithout a rebuild. A file LIEF cannot parse is also a failure rather than a silent pass.RUN_STDLIB_PATH_CHECKdefaults to false, and onlylinux64andmacopt in. That is a default rather than a hard exclusion - a target that inherited the variable as true would run the check - but no target sets it today. Sanitizer and fuzz builds are deliberately not opted in: their instrumentation records source locations for its own diagnostics and is expected to name standard library headers legitimately, so enabling it there would report instrumentation as a defect. They can be revisited once we know what they actually embed.How Has This Been Tested?
The check found a real second bug on its first run, which is the strongest evidence for it. After #7760 the linux release binaries still embedded
bits/stl_construct.h; that turned out to be a separate defect ingsl::not_null, fixed in #7772, which source review had missed.Against real artifacts:
dashd,dash-qtandtest_dash- precisely the three binaries that differed between builders on rc.1 - and passes ondash-cli,dash-tx,dash-utilanddash-wallet, precisely the four that were byte-identical__TEXT,__cstringdashd,test_dash,dash-qtanddash-cliThe two halves were separated across branches so each can be observed on its own, since this PR on its own is expected to fail until #7772 merges:
check-stdlib-paths- this PR, the check without the fix. Regular CIlinux64and the guix libstdc++ hosts fail on/usr/include/c++/13/bits/stl_construct.h;macand both guix darwin hosts pass. That asymmetry is the coverage point above: clang drops the branch that keeps the string alive, gcc does not.check-stdlib-paths-with-fix- this PR rebased on top of fix: don't re-capture a source_location when a not_null is relocated #7772, kept purely as evidence and not proposed for merge. Every guix host passes -x86_64-linux-gnu,riscv64,aarch64,powerpc64,x86_64-w64-mingw32and both darwin - along withlinux64andmacin regular CI.Taken together: the check fails on a tree with the bug, passes on a tree with the fix, and produces no false positives across four object-format and standard-library combinations.
Breaking Changes
None at runtime. This adds a build-time check; a build that would have produced a binary embedding such a path now fails instead.
Checklist:
🤖 Generated with Claude Code