Alter FailFast behaviour of memcpy (#526)

This commit changes the codegen for error messages for failed memcpys.
This no longer generates a stack frame and correctly tail calls the
error messages generator.

It also turns the error messages on in Release builds.  This will lead
to better adoption experience.
This commit is contained in:
Matthew Parkinson
2022-05-19 14:20:45 +01:00
committed by GitHub
parent 87d71cce21
commit c445de8eb4
3 changed files with 66 additions and 69 deletions

View File

@@ -22,15 +22,16 @@ namespace snmalloc
* error. With it set to false we print a helpful error message and then crash
* the process. The process may be in an undefined state by the time the
* check fails, so there are potentially security implications to turning this
* off. It defaults to true for debug builds, false for release builds and
* can be overridden by defining the macro `SNMALLOC_FAIL_FAST` to true or
* false.
* off. It defaults to false and can be overridden by defining the macro
* `SNMALLOC_FAIL_FAST` to true.
*
* Current default to true will help with adoption experience.
*/
static constexpr bool FailFast =
#ifdef SNMALLOC_FAIL_FAST
SNMALLOC_FAIL_FAST
#else
!DEBUG
false
#endif
;
@@ -40,71 +41,63 @@ namespace snmalloc
* `p` is the input pointer and `len` is the offset from this pointer of the
* bounds. `msg` is the message that will be reported along with the
* start and end of the real object's bounds.
*/
SNMALLOC_SLOW_PATH SNMALLOC_UNUSED_FUNCTION inline void
report_fatal_bounds_error [[noreturn]] (
void* p, size_t len, const char* msg, decltype(ThreadAlloc::get())& alloc)
{
report_fatal_error(
"{}: {} is in allocation {}--{}, offset {} is past the end\n",
msg,
p,
alloc.template external_pointer<Start>(p),
alloc.template external_pointer<OnePastEnd>(p),
len);
}
/**
* The direction for a bounds check.
*/
enum class CheckDirection
{
/**
* A read bounds check, performed only when read checks are enabled.
*/
Read,
/**
* A write bounds check, performed unconditionally.
*/
Write
};
/**
* Check whether a pointer + length is in the same object as the pointer.
* Fail with the error message from the third argument if not.
*
* The template parameter indicates whether this is a read. If so, this
* function is a no-op when `CheckReads` is false.
* Note that this function never returns. We do not mark it [[NoReturn]]
* so as to generate better code, because [[NoReturn]] prevents tailcails
* in GCC and Clang.
*
* The function claims to return a FakeReturn, this is so it can be tail
* called where the bound checked function returns a value, for instance, in
* memcpy it is specialised to void*.
*/
template<
CheckDirection Direction = CheckDirection::Write,
bool CheckBoth = CheckReads>
SNMALLOC_FAST_PATH_INLINE void
check_bounds(const void* ptr, size_t len, const char* msg = "")
template<typename FakeReturn = void*>
SNMALLOC_SLOW_PATH SNMALLOC_UNUSED_FUNCTION inline FakeReturn
report_fatal_bounds_error(const void* ptr, size_t len, const char* msg)
{
if constexpr ((Direction == CheckDirection::Write) || CheckBoth)
if constexpr (FailFast)
{
UNUSED(ptr, len, msg);
SNMALLOC_FAST_FAIL();
}
else
{
auto& alloc = ThreadAlloc::get();
void* p = const_cast<void*>(ptr);
if (SNMALLOC_UNLIKELY(!alloc.check_bounds(ptr, len)))
{
if constexpr (FailFast)
{
UNUSED(p, len, msg);
SNMALLOC_FAST_FAIL();
}
else
{
report_fatal_bounds_error(p, len, msg, alloc);
}
}
}
else
{
UNUSED(ptr, len, msg);
auto range_end = pointer_offset(p, len);
auto object_end = alloc.template external_pointer<OnePastEnd>(p);
report_fatal_error(
"Fatal Error!\n{}: \n\trange [{}, {})\n\tallocation [{}, "
"{})\nrange goes beyond allocation by {} bytes \n",
msg,
p,
range_end,
alloc.template external_pointer<Start>(p),
object_end,
pointer_diff(object_end, range_end));
}
}
/**
* Check whether a pointer + length is in the same object as the pointer.
*
* Returns true if the checks succeeds.
*
* The template parameter indicates whether the check should be performed. It
* defaults to true. If it is false, the check will always succeed.
*/
template<bool PerformCheck = true>
SNMALLOC_FAST_PATH_INLINE bool check_bounds(const void* ptr, size_t len)
{
if constexpr (PerformCheck)
{
auto& alloc = ThreadAlloc::get();
return alloc.check_bounds(ptr, len);
}
else
{
UNUSED(ptr, len);
return true;
}
}
} // namespace snmalloc

View File

@@ -317,14 +317,13 @@ namespace snmalloc
return dst;
}
if constexpr (Checked)
{
// Check the bounds of the arguments.
check_bounds(
dst, len, "memcpy with destination out of bounds of heap allocation");
check_bounds<CheckDirection::Read, CheckReads>(
// Check the bounds of the arguments.
if (SNMALLOC_UNLIKELY(!check_bounds<(Checked && ReadsChecked)>(src, len)))
return report_fatal_bounds_error(
src, len, "memcpy with source out of bounds of heap allocation");
}
if (SNMALLOC_UNLIKELY(!check_bounds<Checked>(dst, len)))
return report_fatal_bounds_error(
dst, len, "memcpy with destination out of bounds of heap allocation");
Arch::copy(dst, src, len);
return orig_dst;

View File

@@ -90,7 +90,12 @@ void memcpy_unchecked(void* dst, const void* src, size_t size)
NOINLINE
void memcpy_platform_checked(void* dst, const void* src, size_t size)
{
check_bounds(dst, size, "");
if (SNMALLOC_UNLIKELY(!check_bounds(dst, size)))
{
report_fatal_bounds_error(dst, size, "");
return;
}
memcpy(dst, src, size);
}