Skip to content

std::copy and others bypass _LIBCPP_ABI_BOUNDED_ITERATORS and other hardened iterators #78771

Open
@davidben

Description

std::copy has a memmove specialization for contiguous iterators. However, this specialization causes hardened iterator types, like _LIBCPP_ABI_BOUNDED_ITERATORS to lose their safety checks. This is particularly unfortunate because, as far as I can tell, the simplest way in C++20 to copy between spans is:

void copy_span(std::span<const int> src, std::span<int> dst) {
    std::ranges::copy(src, dst.begin());
}

This is bounds-safe with respect to src, because std::ranges takes care of ensuring we act on src.begin() and src.end() as a pair, but not with respect dst. There is no API that takes both input and output as range, as far as I can tell. That means our only option for safety is if dst.begin() returning an iterator type that is aware of its bounds. With libc++ bounded iterators, it does so. However, the specialization discards the optimization:

https://godbolt.org/z/scTvebr76

std::copy_n has a similar issue because it takes a count, so neither source nor destination pass their bounds.

Looking at libc++ internals, I think the problem is two-fold:

First, __bounded_iter does not check operator+, only operator*, so element-by-element dereference is the only hope we have to bounds-check things. But that's quite incompatible with the memcpy optimization. Also, as we see from copy_span2, Clang can't figure out to hoist the checks out of the loop. I suspect it doesn't help that __libcpp_verbose_abort forces different asserts to emit different messages. Although when I replace it with __builtin_trap, it still checks on every loop iteration.

Second, even if libc++ (or an external hardened iterator) checked operator+, it doesn't run operator+ on the output iterator before the memmove. We do actually run it after the memmove in __rewrap_iter, but by that point it's too late and we've written out of bounds.
https://github.com/llvm/llvm-project/blob/main/libcxx/include/__algorithm/copy_move_common.h#L104-L108

So I think the way to fix this is:

  1. Make __bounded_iter bounds-check operator+ and friends
  2. Once we've determined the iterator is unwrappable, compute n = std::distance(first, last). Then compute __out_first + n and throw away the result. We're just trying to trigger hardening checks.
  3. Optional: as a debug check (but not a hardening check because it should never happen and we don't want to actually emit it), assert that n matches the actual number of elements copied.

Update: In addition to the above issue, #78771 (comment) describes a second issue with the various __unwrap_iter optimizations.

Metadata

Assignees

No one assigned

    Labels

    hardeningIssues related to the hardening effortlibc++libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions