...
Code Block | ||||
---|---|---|---|---|
| ||||
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 | ||||
---|---|---|---|---|
| ||||
#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 | ||||
---|---|---|---|---|
| ||||
#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.
...