Add memcpy with bounds checks.

The memcpy implementation is not completely stupid but is almost
certainly not as good as a carefully tuned and optimised one.

Building snmalloc with FreeBSD's libc memcpy + jemalloc and with this,
each 10 times, does not show a statistically significant performance
difference at 95% confidence.  The snmalloc version has very slightly
lower median and worst-case times.  This is in no way a sensible
benchmark, but it serves as a smoke test for significant performance
regressions.

The CI self-host job now uses the checked memcpy.

This also fixes an off-by-one error in the external bounds.  This is
triggered by ninja, so we will see breakage in CI if it is reintroduced.

In debug builds, we provide a verbose error containing the address of
the allocation, the base and bounds of the allocation, and a backtrace.

The backtrace was broken by the CI cleanup moving the BACKTRACE_HEADER
macro into the SNMALLOC_ namespace.  This is also fixed.

The test involves hijacking `abort`, which doesn't work everywhere.  It
also requires `backtrace` to work in configurations where stack traces
are enabled.  This is disabled in QEMU because `backtrace` appears to
crash reliably in QEMU user mode.

For now, in the -checks build configurations, we are hitting a slow path
in the pagemap on accesses so that the pages that are `PROT_NONE` don't
cause crashes.  These need to be made read-only, but this requires a PAL
change.
This commit is contained in:
David Chisnall
2021-08-10 11:36:53 +01:00
committed by David Chisnall
parent d524ef5cac
commit 51e75bca89
12 changed files with 482 additions and 59 deletions

View File

@@ -0,0 +1,155 @@
// Windows doesn't like changing the linkage spec of abort.
#if defined(_MSC_VER)
int main()
{
return 0;
}
#else
// QEMU user mode does not support the code that generates backtraces and so we
// also need to skip this test if we are doing a debug build and targeting
// QEMU.
# if defined(SNMALLOC_QEMU_WORKAROUND) && defined(SNMALLOC_BACKTRACE_HEADER)
# undef SNMALLOC_BACKTRACE_HEADER
# endif
# ifdef SNMALLOC_STATIC_LIBRARY_PREFIX
# undef SNMALLOC_STATIC_LIBRARY_PREFIX
# endif
# ifdef SNMALLOC_FAIL_FAST
# undef SNMALLOC_FAIL_FAST
# endif
# define SNMALLOC_FAIL_FAST false
# define SNMALLOC_STATIC_LIBRARY_PREFIX my_
# include "ds/defines.h"
# ifndef SNMALLOC_PASS_THROUGH
# include "override/malloc.cc"
# else
# define my_malloc(x) malloc(x)
# define my_free(x) free(x)
# endif
# include "override/memcpy.cc"
# include <assert.h>
# include <csetjmp>
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
/**
* Jump buffer used to jump out of `abort()` for recoverable errors.
*/
static std::jmp_buf jmp;
/**
* Flag indicating whether `jmp` is valid. If this is set then calls to
* `abort` will jump to the jump buffer, rather than exiting.
*/
static bool can_longjmp;
/**
* Replacement for the C standard `abort` that returns to the `setjmp` call for
* recoverable errors.
*/
extern "C" void abort()
{
if (can_longjmp)
{
longjmp(jmp, 1);
}
exit(-1);
}
/**
* Check that memcpy works in correct use. This allocates a pair of buffers,
* fills one with a well-known pattern, and then copies subsets of this at
* one-byte increments to a target. This gives us unaligned starts.
*/
void check_size(size_t size)
{
auto* s = reinterpret_cast<unsigned char*>(my_malloc(size + 1));
auto* d = reinterpret_cast<unsigned char*>(my_malloc(size + 1));
d[size] = 0;
s[size] = 255;
for (size_t start = 0; start < size; start++)
{
unsigned char* src = s + start;
unsigned char* dst = d + start;
size_t sz = (size - start);
for (size_t i = 0; i < sz; ++i)
{
src[i] = static_cast<unsigned char>(i);
}
for (size_t i = 0; i < sz; ++i)
{
dst[i] = 0;
}
my_memcpy(dst, src, sz);
for (size_t i = 0; i < sz; ++i)
{
if (dst[i] != static_cast<unsigned char>(i))
{
fprintf(
stderr,
"Testing size %zd %hhx == %hhx\n",
sz,
static_cast<unsigned char>(i),
dst[i]);
}
SNMALLOC_CHECK(dst[i] == (unsigned char)i);
}
SNMALLOC_CHECK(d[size] == 0);
}
my_free(s);
my_free(d);
}
void check_bounds(size_t size, size_t out_of_bounds)
{
auto* s = reinterpret_cast<unsigned char*>(my_malloc(size));
auto* d = reinterpret_cast<unsigned char*>(my_malloc(size));
for (size_t i = 0; i < size; ++i)
{
s[i] = static_cast<unsigned char>(i);
}
for (size_t i = 0; i < size; ++i)
{
d[i] = 0;
}
bool bounds_error = false;
can_longjmp = true;
if (setjmp(jmp) == 0)
{
my_memcpy(d, s, size + out_of_bounds);
}
else
{
bounds_error = true;
}
can_longjmp = false;
SNMALLOC_CHECK(bounds_error == (out_of_bounds > 0));
my_free(s);
my_free(d);
}
int main()
{
// Skip the checks that expect bounds checks to fail when we are not the
// malloc implementation.
# if !defined(SNMALLOC_PASS_THROUGH)
// Some sizes to check for out-of-bounds access
std::initializer_list<size_t> sizes = {16, 1024, 2 * 1024 * 1024};
for (auto sz : sizes)
{
// Check in bounds
check_bounds(sz, 0);
// Check one byte out
check_bounds(sz, 1);
// Check one object out of bounds
check_bounds(sz, sz);
}
# endif
for (size_t x = 0; x < 2048; x++)
{
check_size(x);
}
}
#endif