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

    No conflicts as of last run.

    <!--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
  9. in src/span.h:120 in b5a86b5745
     117 | @@ -108,7 +118,12 @@ concept BasicByte = requires { UCharCast(std::span<B>{}.data()); };
     118 |  template <typename T, size_t N> constexpr auto UCharSpanCast(std::span<T, N> s) { return std::span<std::remove_pointer_t<decltype(UCharCast(s.data()))>, N>{UCharCast(s.data()), s.size()}; }
     119 |  
     120 |  /** Like the std::span constructor, but for (const) unsigned char member types only. Only works for (un)signed char containers. */
    


    maflcko commented at 7:44 AM on September 23, 2026:

    unrelated: Seems odd to require BasicByte here for MakeUCharSpan, but not for MakeByteSpan.

    Either it should be removed for both, or enforced for both.

    Maybe it can be made explicit with something like:

    #include <span>
    #include <cstddef>
    
    enum class EndianPolicy {
        Strict,
        Ignore
    };
    
    template <EndianPolicy Policy = EndianPolicy::Strict, typename V>
    auto MakeByteSpan(const V& v) noexcept
    {
        if constexpr (sizeof(typename decltype(std::span{v})::element_type) > 1) {
            static_assert(Policy == EndianPolicy::Ignore,
                "Cannot create byte span for multi-byte types: byte order is architecture-dependent. "
                "Use 'MakeByteSpan<EndianPolicy::Ignore>(...)' to acknowledge this.");
        }
    
        return std::as_bytes(std::span{v});
    }
    

    Also, UCharSpanCast is only called/used once, so maybe it can be renamed to UCharCast?

    Afterwards, MakeUCharSpan(a) could even be removed and replaced by UCharCast(MakeByteSpan(a))?


    maflcko commented at 10:28 AM on September 23, 2026:

    also, reminds me that UCharCast will eat the annotation anyway right now. At least I don't get any warning for:

    diff --git a/src/test/span_tests.cpp b/src/test/span_tests.cpp
    index 42b62413aa..f0a22636d6 100644
    --- a/src/test/span_tests.cpp
    +++ b/src/test/span_tests.cpp
    @@ -44,0 +45,6 @@ BOOST_AUTO_TEST_SUITE(span_tests)
    +auto test_function()
    +{
    +    std::vector<std::byte> a;
    +    return UCharSpanCast(std::span{a});
    +}
    +
    

    So this pull is incomplete anyway

  10. maflcko commented at 7:47 AM on September 23, 2026: member

    Just wondering about the great picture of this file here (left an unrelated comment).


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-10-12 01:51 UTC

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