span: diagnose dangling views from MakeByteSpan/MakeUCharSpan #36183

pull kevkevinpal wants to merge 1 commits into bitcoin:master from kevkevinpal:span-lifetimebound-make-byte-uchar changing 2 files +23 −1
  1. kevkevinpal commented at 2:35 PM on September 7, 2026: contributor

    Follow-up to #36164, as suggested in #36164 (comment).

    Tracked in https://github.com/kevkevinpal/bitcoin/issues/550.

    Problem: MakeByteSpan and MakeUCharSpan return views into their input. A temporary container can leave those views dangling.

    Fix: Add LIFETIMEBOUND to the overload taking non-borrowed_range types, so Clang diagnoses the misuse on owning containers without flagging correct code that passes a view. leveldb::Slice is opted into borrowed_range so dbwrapper can keep using the helper. Matching the util/string.h helpers from #36164.

  2. DrahtBot commented at 2:35 PM on September 7, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/36183.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept ACK jeanpablojp

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34132 (coins, dbwrapper: remove error catcher, make point-read failures fatal by l0rinc)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  3. jeanpablojp commented at 10:31 PM on September 7, 2026: contributor

    Concept ACK

    LIFETIMEBOUND on const V& claims the result borrows from v. True when V owns the bytes, false when V is a non-owning view, and MakeByteSpan takes both, so the annotation also fires on correct code. The four warnings this diff works around are all that case.

    It also reaches a shape outside the diff, where a function returning MakeByteSpan of a local std::span or std::string_view now gets -Wreturn-stack-address.

    Would a requires (!std::ranges::borrowed_range<V>) on the annotated overload work? Compiled both ways here, the constrained form keeps the temporary-container warning and drops the two in net_tests. leveldb::Slice is not a borrowed range, so dbwrapper needs its own handling regardless.

  4. span: diagnose dangling views from MakeByteSpan/MakeUCharSpan
    Follow-up to #36164. Add LIFETIMEBOUND on the non-borrowed-range
    overload so Clang diagnoses storing spans of temporary owning
    containers, matching the util/string.h helpers.
    b5a86b5745
  5. in src/span.h:86 in 6591bf02ac outdated
      82 | @@ -81,7 +83,7 @@ T& SpanPopBack(std::span<T>& span)
      83 |  }
      84 |  
      85 |  template <typename V>
      86 | -auto MakeByteSpan(const V& v) noexcept
      87 | +auto MakeByteSpan(const V& v LIFETIMEBOUND) noexcept
    


    jeanpablojp commented at 10:31 PM on September 7, 2026:

    Concretely this, plus a #include <ranges> at the top:

    template <typename V> requires std::ranges::borrowed_range<V>
    auto MakeByteSpan(const V& v) noexcept
    {
        return std::as_bytes(std::span{v});
    }
    template <typename V> requires (!std::ranges::borrowed_range<V>)
    auto MakeByteSpan(const V& v LIFETIMEBOUND) noexcept
    {
        return std::as_bytes(std::span{v});
    }
    

    The first overload takes whatever std::ranges::borrowed_range accepts, std::span and std::string_view among them, and everything else takes the second. MakeUCharSpan takes the same pair, with the requires sitting alongside its decltype return.


    kevkevinpal commented at 8:33 PM on September 17, 2026:

    Taken as-is, with #include <ranges> added. Only difference is that clang-format puts the requires-clause on its own line rather than inline with the decltype return for MakeUCharSpan.

  6. in src/dbwrapper.cpp:419 in 6591bf02ac
     415 | @@ -416,12 +416,14 @@ std::span<const std::byte> CDBIterator::GetKeyImpl() const
     416 |  {
     417 |      // The returned span borrows from the current iterator entry and is only
     418 |      // valid until the iterator is advanced.
     419 | -    return MakeByteSpan(m_impl_iter->iter->key());
     420 | +    const leveldb::Slice key{m_impl_iter->iter->key()};
    


    jeanpablojp commented at 10:31 PM on September 7, 2026:

    Neither form keeps the helper here. MakeByteSpan(m_impl_iter->iter->key()) warns because the Slice is a temporary, and binding it to a local only changes the warning to "address of stack memory associated with local variable 'key' returned".

    Would opting the Slice in be worth it? On top of those overloads it brings both getters back to the helper, and a stored MakeByteSpan of a temporary std::vector still warns. On its own it changes nothing, since the annotation as it stands is unconditional.

    template<> inline constexpr bool std::ranges::enable_borrowed_range<leveldb::Slice> = true;
    

    kevkevinpal commented at 8:34 PM on September 17, 2026:

    Opted it in, and it turns out to be load-bearing rather than just a cleanup. With the constrained overloads but no specialization, clang gives -Wreturn-stack-address on both getters, since Slice is not a borrowed_range and lands on the annotated overload. With the specialization both getters go back to MakeByteSpan and the file is clean.

    I kept it in dbwrapper.cpp rather than the header: dbwrapper.h deliberately doesn't pull in leveldb headers, and this is the only TU that pairs Slice with the span helpers.

  7. in src/test/serialize_tests.cpp:452 in 6591bf02ac
     447 | @@ -448,7 +448,8 @@ BOOST_AUTO_TEST_CASE(with_params_multi)
     448 |  //! Test creating a ParamsStream that moves from a stream argument.
     449 |  BOOST_AUTO_TEST_CASE(with_params_move)
     450 |  {
     451 | -    UncopyableStream stream{MakeByteSpan(std::string_view{"abc"})};
     452 | +    constexpr std::string_view abc{"abc"};
     453 | +    UncopyableStream stream{MakeByteSpan(abc)};
    


    jeanpablojp commented at 10:31 PM on September 7, 2026:

    UncopyableStream inherits the DataStream constructor that copies into vch, so no view is kept here. Reverted, the file still compiles clean.

        UncopyableStream stream{MakeByteSpan(std::string_view{"abc"})};
    

    kevkevinpal commented at 8:32 PM on September 17, 2026:

    Reverted, thanks, you're right that UncopyableStream copies into vch, so no view outlives the temporary.

  8. kevkevinpal force-pushed on Sep 17, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-09-21 10:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me