Issue 131718
Summary [libc++] {std, ranges}::copy_backward fails to copy vector<bool> correctly with small storage types
Labels libc++
Assignees
Reporter winner245
    The current implementation of `std::copy_backward` and `std::ranges::copy_backward` in libc++ incorrectly copy  `vector<bool>` when the underlying storage type (`__storage_type`) is smaller than `int` (e.g., `unsigned char`, `unsigned short`, `uint8_t` and `uint16_t`). Specifically, the copying operation can unintentionally zero untargeted bits, resulting in data corruption.


#### Reproducer
Here’s a minimal reproducer ([Godbolt Link](https://godbolt.org/z/6ndfGbYhY)):

```cpp
#include <limits>
#include <memory>
#include <vector>
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <iostream>

template <typename T, typename Size, typename Difference>
class sized_allocator {
  template <typename U, typename Sz, typename Diff>
  friend class sized_allocator;

public:
  using value_type                  = T;
  using size_type                   = Size;
  using difference_type             = Difference;
  using propagate_on_container_swap = std::true_type;

  constexpr explicit sized_allocator(int d = 0) : data_(d) {}

  template <typename U, typename Sz, typename Diff>
  constexpr sized_allocator(const sized_allocator<U, Sz, Diff>& a) noexcept : data_(a.data_) {}

  constexpr T* allocate(size_type n) {
    if (n > max_size())
      throw std::bad_array_new_length();
    return std::allocator<T>().allocate(n);
  }

  constexpr void deallocate(T* p, size_type n) noexcept { std::allocator<T>().deallocate(p, n); }

  constexpr size_type max_size() const noexcept {
    return std::numeric_limits<size_type>::max() / sizeof(value_type);
  }

private:
  int data_;

  constexpr friend bool operator==(const sized_allocator& a, const sized_allocator& b) {
    return a.data_ == b.data_;
  }
  constexpr friend bool operator!=(const sized_allocator& a, const sized_allocator& b) {
    return a.data_ != b.data_;
  }
};

int main() { 
  using Alloc = sized_allocator<bool, std::uint16_t, std::int16_t>;
  std::vector<bool, Alloc> in(16, false, Alloc(1));
  std::vector<bool, Alloc> out(16, true, Alloc(1));
  std::copy_backward(in.begin(), in.begin() + 1, out.begin() + 1); // out[0] = false; but entire word zeroed
  for (std::size_t i = 0; i < out.size(); ++i)
    std::cout << out[i];
}
```
 
##### Expected copying behavior:
- Only a single bit `out[0]` is set to `false`, while `out[1..15]` are untouched and should remain unchanged.
- Expected output: `01111111 11111111`.

##### Current erroneous copying behavior:
- The entire storage word is zeroed, setting all bits in `out[0..15]` to `false`.
- Incorrect output: `00000000 00000000`.


#### Root Cause

The erroneous copying behavior stems from the flawed bit mask computation in `std::copy_backward`: 

https://github.com/llvm/llvm-project/blob/e758237352f70fad028f3947e6f0404e50fec024/libcxx/include/__algorithm/copy_backward.h#L55

where
- `~__storage_type(0)` is first integral-promoted to `int(-1)`;
- `int(-1)` is then left-shifted and right-shifted;
   - Pre-C++20: Left-shifting a negative value is UB before C++20 ([[expr.shift]/3](https://eel.is/c++draft/expr.shift#note-2));
   - C++20 and later: right-shifting a negative value results in sign-bit extension since C++20 ([cppreference](https://en.cppreference.com/w/cpp/language/operator_arithmetic#Bitwise_shift_operators)). 
- Due to sign-bit extension, the bit mask becomes all `1`s instead of targeting specific bits, causing the entire word to be zeroed.

Clang consistently extends sign bits in right shifts even pre-C++20, making the bug reproducible across standards.
Additionally, several other bitwise operations in `copy_backward` suffer from similar promotion and shifting issues. 
_______________________________________________
llvm-bugs mailing list
llvm-bugs@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs

Reply via email to