std::copy and others bypass _LIBCPP_ABI_BOUNDED_ITERATORS and other hardened iterators #78771
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:
- Make
__bounded_iter
bounds-checkoperator+
and friends - 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. - 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.