Commit 90cc0771 by Patrick Steinhardt Committed by Edward Thomson

tests: add allocator with limited number of bytes

In several circumstances, we get bug reports about things that happen in
situations where the environment is quite limited with regards to
available memory. While it's expected that functionality will fail if
memory allocations fail, the assumption is that we should do so in a
controlled way. Most importantly, we do not want to crash hard due to
e.g. accessing NULL pointers.

Naturally, it is quite hard to debug such situations. But since our
addition of pluggable allocators, we are able to implement allocators
that fail in deterministic ways, e.g. after a certain amount of bytes
has been allocated. This commit does exactly that.

To be able to properly keep track of the amount of bytes currently
allocated, allocated pointers contain tracking information. This
tracking information is currently limited to the number of bytes
allocated, so that we can correctly replenish them on calling `free` on
the pointer. In the future, it would be feasible to extend the tracked
information even further, e.g. by adding information about file and line
where the allocation has been performed. As this introduced some
overhead to allocations though, only information essential to limited
allocations is currently tracked.
parent 9dd1bfe8
......@@ -75,10 +75,23 @@ git_threadstate *git_threadstate_get(void)
if ((threadstate = git_tlsdata_get(tls_key)) != NULL)
return threadstate;
if ((threadstate = git__calloc(1, sizeof(git_threadstate))) == NULL ||
git_str_init(&threadstate->error_buf, 0) < 0)
/*
* Avoid git__malloc here, since if it fails, it sets an error
* message, which requires thread state, which would allocate
* here, which would fail, which would set an error message...
*/
if ((threadstate = git__allocator.gmalloc(sizeof(git_threadstate),
__FILE__, __LINE__)) == NULL)
return NULL;
memset(threadstate, 0, sizeof(git_threadstate));
if (git_str_init(&threadstate->error_buf, 0) < 0) {
git__allocator.gfree(threadstate);
return NULL;
}
git_tlsdata_set(tls_key, threadstate);
return threadstate;
}
/*
* Copyright (C) the libgit2 contributors. All rights reserved.
*
* This file is part of libgit2, distributed under the GNU GPL v2 with
* a Linking Exception. For full terms see the included COPYING file.
*/
#include "clar_libgit2_alloc.h"
static size_t bytes_available;
/*
* The clar allocator uses a tagging mechanism for pointers that
* prepends the actual pointer's number bytes as `size_t`.
*
* First, this is required in order to be able to implement
* proper bookkeeping of allocated bytes in both `free` and
* `realloc`.
*
* Second, it may also be able to spot bugs that are
* otherwise hard to grasp, as the returned pointer cannot be
* free'd directly via free(3P). Instead, one is forced to use
* the tandem of `cl__malloc` and `cl__free`, as otherwise the
* code is going to crash hard. This is considered to be a
* feature, as it helps e.g. in finding cases where by accident
* malloc(3P) and free(3P) were used instead of git__malloc and
* git__free, respectively.
*
* The downside is obviously that each allocation grows by
* sizeof(size_t) bytes. As the allocator is for testing purposes
* only, this tradeoff is considered to be perfectly fine,
* though.
*/
static void *cl__malloc(size_t len, const char *file, int line)
{
char *ptr = NULL;
size_t alloclen;
GIT_UNUSED(file);
GIT_UNUSED(line);
if (len > bytes_available)
goto out;
if (GIT_ADD_SIZET_OVERFLOW(&alloclen, len, sizeof(size_t)) ||
(ptr = malloc(alloclen)) == NULL)
goto out;
memcpy(ptr, &len, sizeof(size_t));
bytes_available -= len;
out:
return ptr ? ptr + sizeof(size_t) : NULL;
}
static void cl__free(void *ptr)
{
if (ptr) {
char *p = ptr;
size_t len;
memcpy(&len, p - sizeof(size_t), sizeof(size_t));
free(p - sizeof(size_t));
bytes_available += len;
}
}
static void *cl__realloc(void *ptr, size_t size, const char *file, int line)
{
size_t copybytes = 0;
char *p = ptr;
void *new;
if (p)
memcpy(&copybytes, p - sizeof(size_t), sizeof(size_t));
if (copybytes > size)
copybytes = size;
if ((new = cl__malloc(size, file, line)) == NULL)
goto out;
memcpy(new, p, copybytes);
cl__free(p);
out:
return new;
}
void cl_alloc_limit(size_t bytes)
{
git_allocator alloc;
alloc.gmalloc = cl__malloc;
alloc.grealloc = cl__realloc;
alloc.gfree = cl__free;
git_allocator_setup(&alloc);
bytes_available = bytes;
}
void cl_alloc_reset(void)
{
git_allocator stdalloc;
git_stdalloc_init_allocator(&stdalloc);
git_allocator_setup(&stdalloc);
}
#ifndef __CLAR_LIBGIT2_ALLOC__
#define __CLAR_LIBGIT2_ALLOC__
#include "clar.h"
#include "common.h"
#include "git2/sys/alloc.h"
void cl_alloc_limit(size_t bytes);
void cl_alloc_reset(void);
#endif
#include "clar_libgit2.h"
#include "clar_libgit2_alloc.h"
#include "alloc.h"
void test_alloc__cleanup(void)
{
cl_alloc_reset();
}
void test_alloc__oom(void)
{
void *ptr = NULL;
cl_alloc_limit(0);
cl_assert(git__malloc(1) == NULL);
cl_assert(git__calloc(1, 1) == NULL);
cl_assert(git__realloc(ptr, 1) == NULL);
cl_assert(git__strdup("test") == NULL);
cl_assert(git__strndup("test", 4) == NULL);
}
void test_alloc__single_byte_is_exhausted(void)
{
void *ptr;
cl_alloc_limit(1);
cl_assert(ptr = git__malloc(1));
cl_assert(git__malloc(1) == NULL);
git__free(ptr);
}
void test_alloc__free_replenishes_byte(void)
{
void *ptr;
cl_alloc_limit(1);
cl_assert(ptr = git__malloc(1));
cl_assert(git__malloc(1) == NULL);
git__free(ptr);
cl_assert(ptr = git__malloc(1));
git__free(ptr);
}
void test_alloc__realloc(void)
{
char *ptr = NULL;
cl_alloc_limit(3);
cl_assert(ptr = git__realloc(ptr, 1));
*ptr = 'x';
cl_assert(ptr = git__realloc(ptr, 1));
cl_assert_equal_i(*ptr, 'x');
cl_assert(ptr = git__realloc(ptr, 2));
cl_assert_equal_i(*ptr, 'x');
cl_assert(git__realloc(ptr, 2) == NULL);
cl_assert(ptr = git__realloc(ptr, 1));
cl_assert_equal_i(*ptr, 'x');
git__free(ptr);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment