Repository navigation
fix: don't re-capture a source_location when a not_null is relocated - #7772
Conversation
not_null's converting constructor takes a defaulted nostd::source_location, and the only other constructor alongside it was `not_null(const not_null&)`. For an rvalue or a non-const lvalue, the template is an exact match while the copy constructor needs an added const, so overload resolution picked the template: relocating a not_null re-evaluated source_location::current() at whatever code moved it, and re-ran the Expects() null check on a value already known to be non-null. When the relocation happens inside a standard library header, that header's path is what gets recorded. std::stack<gsl::not_null<const CBlockIndex*>> in evo/creditpool.cpp moves its element into a freshly allocated deque node, so the linux release binaries embed /usr/include/c++/13/bits/stl_construct.h and would report it as the origin of a failed precondition. Confirmed by symbolizing the only reference to that string in dashd, which resolves through _M_push_back_aux and _M_allocate_node to a source_location built with file stl_construct.h and line 97, followed by the null-check branch. The darwin binaries are clean only because clang drops the unreachable terminate() path that keeps the string alive; gcc does not. Exclude every not_null specialisation from that constructor, through a trait rather than is_same, so that converting between specialisations does not select it either. Relocation then binds to the copy constructor. Deliberately no move constructor is added. Moving would leave a smart-pointer T null and break the invariant the type exists to enforce, which is why upstream omits one and why strict_not_null's own move can be defaulted - it copies the base rather than moving it, as its comment says. The not_null<U> to not_null<T> constructor no longer delegates, which recorded this header as the origin of any failure, but keeps its check: U is non-null, yet nothing constrains a U to T conversion to preserve that. It takes its own location so the check names the caller. Two assertions pin the result. The first fails if the location-capturing constructor is selected for a relocation again, since it cannot be noexcept while it calls Expects(). The second fails if a move constructor is ever added, which the first would not catch - a defaulted move constructor satisfies it too, so a move-only T that must not be movable is what detects one. 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. |
|
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 (1)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review. Walkthrough
Priority: ⬇️ Low Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: Merge Risk: ⚪ Minimal · up to The change preserves non-null checks and smart-pointer source state while correcting location attribution during relocation. No actionable merge-blocking risk is established; merge after normal checks pass. Architecture SummaryArchitecture risk: 🔵 Low · up to The change affects 1 system. Changed systems: Architecture concerns Review detailsSystems and components
Before / after behavior
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
|
✅ Final review complete — no blockers (commit be3c436) · triage: low |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
Verified the combined reviewer evidence against the exact head and merge-base PR diff; no actionable defects were found. Independent C++20 probes confirmed that relocation avoids redundant checks, shared-pointer sources remain non-null, and cross-specialisation conversions retain caller-attributed failure checks. The regression assertions pass at head and fail against the parent header and a move-constructor mutation; the full build and multi-platform results reported in the PR were not independently rerun.
Review provenance
Source: reviewer 1: glm-5.3-flash (agent: phase1-reviewer, role: general); reviewer 2: glm-5.3-flash (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:
lowbygpt-6.1-sol(effort low) — Small, self-contained change to gsl::not_null constructor overload resolution and compile-time regression checks, with no impact on consensus, funds, cryptography, networking, or storage. - Phase 1 reviewers:
glm-5.3-flash— general (completed, effort high); agentphase1-reviewer,glm-5.3-flash— dash-core-commit-history (completed, effort high); agentphase1-reviewer - Phase 1 model:
glm-5.3-flash— zai quota: 5h 100% left, weekly 79% left; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 13% left, 5h 100% left) - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— dash-core-commit-history (completed, effort medium); agentphase2-reviewer
…library header path b6b9b75 ci: run the stdlib header path check on the linux64 and mac builds (UdjinM6) 4471e14 guix: fail the build when a binary embeds a stdlib header path (UdjinM6) Pull request description: ## 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.cpp` and 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.py` parses each executable with LIEF and fails if one embeds a C++ standard library header path, wired as a `check-stdlib-paths` make target beside the existing `check-symbols` and `check-security`, and invoked from the guix build. It also runs in regular CI, behind `RUN_STDLIB_PATH_CHECK`, enabled for the `linux64` and `mac` targets. `check-symbols` could 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 - and `check-security` is 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 a `make distdir` tree (`ci/dash/build_src.sh:29-37`), so the script is listed in `BIN_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 `linux64` should 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.1` are 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 `/81` rather 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 `objdump` without a rebuild. A file LIEF cannot parse is also a failure rather than a silent pass. `RUN_STDLIB_PATH_CHECK` defaults to false, and only `linux64` and `mac` opt 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 in `gsl::not_null`, fixed in #7772, which source review had missed. Against real artifacts: - the pre-fix guix build of 0f87636 fails on `dashd`, `dash-qt` and `test_dash` - precisely the three binaries that differed between builders on rc.1 - and passes on `dash-cli`, `dash-tx`, `dash-util` and `dash-wallet`, precisely the four that were byte-identical - discrimination was checked directly: an object file containing twelve standard library paths in total reports exactly one, the string literal in `__TEXT,__cstring` - no false positives on an ordinary local non-guix build of `dashd`, `test_dash`, `dash-qt` and `dash-cli` The 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`](https://github.com/UdjinM6/dash/commits/refs/heads/check-stdlib-paths/) - this PR, the check without the fix. Regular CI `linux64` and the guix libstdc++ hosts fail on `/usr/include/c++/13/bits/stl_construct.h`; `mac` and 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`](https://github.com/UdjinM6/dash/commits/refs/heads/check-stdlib-paths-with-fix/) - this PR rebased on top of #7772, kept purely as evidence and not proposed for merge. Every guix host passes - `x86_64-linux-gnu`, `riscv64`, `aarch64`, `powerpc64`, `x86_64-w64-mingw32` and both darwin - along with `linux64` and `mac` in 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: - [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: b4941bfb9627910a4152b7b836a1bc92031eeecd5e38bc1765c89dc6509f570933e681193b106461bad01afb560e5ace939c10b0dbc38785aaf8ccf7b44e2a44
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
Relocating a
gsl::not_nullre-records where it was moved, so a failed precondition names whatever code moved it rather than the code that produced the null pointer. When the relocation happens inside a standard library header, that header's path is embedded in the shipped binaries.not_null's converting constructor takes a Dash-specific defaultednostd::source_location loc = nostd::source_location::current()(src/gsl/pointers.h:122-126) and forwards it toExpects(). Upstream Microsoft GSL has no such parameter, so while its own overload resolution behaves the same way, there is nothing there to recapture. The other candidates werenot_null(const not_null&)and the templatednot_null(const not_null<U>&). For an rvalue or a non-const lvalue, the forwarding template bindsU&&exactly while both of those need an addedconst, so overload resolution picked the template, and every relocation re-evaluatedsource_location::current()and re-ran the null check on a value already known to be non-null.That location is printed, not merely stored:
gsl::details::terminate()(src/gsl/assert.cpp:26-37) writesfile_name(),line(),column()andfunction_name()to stderr and todebug.logbeforestd::terminate(), in release builds as well as debug ones.std::stack<gsl::not_null<const CBlockIndex*>>inevo/creditpool.cppmoves its element into a freshly allocated deque node, so the linux release binaries embed/usr/include/c++/13/bits/stl_construct.hand would report it as the origin of a failed precondition. Confirmed by symbolizing the only reference to that string in a releasedashd: it resolves through_M_push_back_auxand_M_allocate_nodeto asource_locationbuilt with filestl_construct.hand line 97, followed by the null-check branch. The darwin binaries are clean only because clang drops the unreachableterminate()path that keeps the string alive; gcc does not.This is the remaining half of #7760. That PR stopped a raw pointer being forwarded into the standard library and converted there; this stops an already-constructed
not_nullre-capturing when a container relocates it. Neither subsumes the other, and with only #7760 the linux binaries still carried the path.What was done?
Excluded every
not_nullspecialisation from the location-capturing constructor, through adetails::is_not_nulltrait rather thanis_same, so that converting between specialisations does not select it either. Relocation then binds to the copy constructor.No move constructor is added, deliberately. Moving would leave a smart-pointer
Tnull and break the invariant the type exists to enforce, which is why upstream omits one and whystrict_not_null's own move can be defaulted - it copies the base rather than moving it, as its comment atsrc/gsl/pointers.h:333-335says.The
not_null<U>tonot_null<T>constructor no longer delegates, which recorded this header as the origin of any failure, but keeps its check:Uis non-null, yet nothing constrains aUtoTconversion to preserve that. It takes its own location so the check names the caller.Two assertions pin the result. The first fails if the location-capturing constructor is selected for a relocation again, since as written it is not
noexcept- it would not catch one that someone explicitly declarednoexcept, which is a smaller hole than leaving it unguarded. The second fails if a usable move constructor is added, which the first would not catch, since a defaulted move constructor satisfies it too; a move-onlyTthat must not be movable is what detects one.Deliberately not addressed, to keep this to the defect:
make_not_nulland thestrict_not_nullconstructors still capture locations insidepointers.h, andstrict_not_nullis not covered by the trait because it derives fromnot_nullrather than being a specialisation. Neither embeds a standard library path, andstrict_not_nullis unused outsidesrc/gsl/.How Has This Been Tested?
No unit test was added. The captured location is observable only by making the precondition fail, which needs a death or subprocess test around a deliberately null pointer; that seemed a poor trade for the compile-time guarantee below. The two
static_asserts are the regression test, and both were checked in each direction: they pass against this header, and fail againstdevelop's header and against a header with a move constructor re-added.Verified by compiling probes that use
#lineto attribute a relocation to a synthetic header:not_null, copying a non-const lvalue, and convertingnot_null<B*>tonot_null<const B*>all recorded the mover's filestd::dequeforcing node allocation embeds no standard library path at allnot_null<std::shared_ptr<int>>was moved and the source then checked at runtime: still non-null, so the invariant holdsFull build plus the
evo_*andllmq_*suites (228 cases) pass locally. The guix build passes on all seven hosts -x86_64-linux-gnu,riscv64,aarch64,powerpc64,x86_64-w64-mingw32and both darwin - which is the real coverage here, since only two of those toolchains are reachable locally.Breaking Changes
None. No consensus, P2P, RPC, wallet or on-disk behaviour changes, and no change to the class layout - the sole data member is still
T. Thenot_null<U>tonot_null<T>constructor gains a defaulted parameter, and overload selection among the inline constructors changes, so this is not source-identical for anything that named those overloads explicitly. For the copyableTused in this tree, raw pointers andshared_ptr, relocation already copied the pointer; it simply no longer re-runs a redundant check or records a location while doing so.Checklist:
🤖 Generated with Claude Code