Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Added a new NCCE/CS pair for uninitialized memory being assigned into

...

Code Block
bgColor#ccccff
langcpp
class S {
  int V;
public:
  S() : V(12) {} // Not a trivial constructor
  void f();
};
 
void f() {
  S s;

  // ...

  goto bad_idea;

  // ...

bad_idea:
  s.f();
}

Noncompliant Code Example

In this noncompliant code example, f() is called with an iterable range of objects of type S. These objects are copied into a temporary buffer using std::copy(), and when processing of those objects is complete, the temporary buffer is deallocated. However, the buffer returned by std::get_temporary_buffer() does not contain initialized objects of type S, so when std::copy() dereferences the destination iterator, it results in undefined behavior because the object being assigned into has yet to start its lifetime.

Code Block
bgColor#FFcccc
langcpp
#include <algorithm>
#include <cstddef>
#include <memory>
#include <type_traits>
 
class S {
  int i;

public:
  S() : i(0) {}
  S(int i) : i(i) {}
  S(const S&) = default;
  S& operator=(const S&) = default;
};

template <typename Iter>
void f(Iter I, Iter E) {
  static_assert(std::is_same<typename std::iterator_traits<Iter>::value_type, S>::value,
                "Expecting iterators over type S");
  ptrdiff_t count = std::distance(I, E);
  if (!count) {
    return;
  }
  
  // Get some temporary memory.
  auto p = std::get_temporary_buffer<S>(count);
  if (p.second < count) {
    // Handle error; memory wasn't allocated, or insufficient memory was allocated.
    return;
  }
  S *vals = p.first; 
  
  // Copy the values into the memory.
  std::copy(I, E, vals);
  
  // ...
  
  // Return the temporary memory.
  std::return_temporary_buffer(vals);    
}

Implementation Details

A reasonable implementation of std::get_temporary_buffer() and std::copy() can result in code that behaves like (error-checking elided):

Code Block
unsigned char *buffer = new (std::nothrow) unsigned char[sizeof(S) * object_count];
S *result = reinterpret_cast<S *>(buffer);
while (I != E) {
  *result = *I; // Undefined behavior
  ++result;
  ++I;
}

The act of dereferencing result is undefined behavior because the memory pointed to is not an object of type S within its lifetime.

Compliant Solution

In this compliant solution, std::uninitialized_copy() is used to perform the copy, instead of std::copy(), ensuring that the objects are initialized using placement new instead of dereferencing uninitialized memory. The compliant solution also shows an alternate solution using a std::raw_storage_iterator, with the same well-defined results.

Code Block
bgColor#ccccff
langcpp
#include <algorithm>
#include <cstddef>
#include <memory>
#include <type_traits>
 
class S {
  int i;
public:
  S() : i(0) {}
  S(int i) : i(i) {}
  S(const S&) = default;
  S& operator=(const S&) = default;
};

template <typename Iter>
void f(Iter I, Iter E) {
  static_assert(std::is_same<typename std::iterator_traits<Iter>::value_type, S>::value,
                "Expecting iterators over type S");
  ptrdiff_t count = std::distance(I, E);
  if (!count) {
    return;
  }
  
  // Get some temporary memory.
  auto p = std::get_temporary_buffer<S>(count);
  if (p.second < count) {
    // Handle error; memory wasn't allocated, or insufficient memory was allocated.
    return;
  }
  S *vals = p.first; 
  
  // Copy the values into the memory.
  std::uninitialized_copy(I, E, vals);
  // It is also acceptable to use a raw_storage_iterator in situations where an iterator
  // over uninitialized objects is required.
  //  std::copy(I, E, std::raw_storage_iterator<S*, S>(vals));
  
  // ...
  
  // Return the temporary memory.
  std::return_temporary_buffer(vals);    
}

Risk Assessment

Referencing an object outside of its lifetime can result in an attacker being able to run arbitrary code.

...