summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Steinhardt <ps@pks.im>2019-10-11 10:57:28 +0200
committerEdward Thomson <ethomson@edwardthomson.com>2023-05-15 09:28:47 +0100
commit90cc07711c43b12a4bb270583026cbc71db3823e (patch)
treeb98211e2015291a64380efe6dda004dd36958490
parent9dd1bfe81c3aba6ce35ed85bf62fa75d9c61e1a6 (diff)
downloadlibgit2-90cc07711c43b12a4bb270583026cbc71db3823e.tar.gz
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.
-rw-r--r--src/libgit2/threadstate.c17
-rw-r--r--tests/clar/clar_libgit2_alloc.c106
-rw-r--r--tests/clar/clar_libgit2_alloc.h11
-rw-r--r--tests/util/alloc.c68
4 files changed, 200 insertions, 2 deletions
diff --git a/src/libgit2/threadstate.c b/src/libgit2/threadstate.c
index 9e3ef5818..ed9bb9b96 100644
--- a/src/libgit2/threadstate.c
+++ b/src/libgit2/threadstate.c
@@ -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;
}
diff --git a/tests/clar/clar_libgit2_alloc.c b/tests/clar/clar_libgit2_alloc.c
new file mode 100644
index 000000000..7abc998ce
--- /dev/null
+++ b/tests/clar/clar_libgit2_alloc.c
@@ -0,0 +1,106 @@
+/*
+ * 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);
+}
diff --git a/tests/clar/clar_libgit2_alloc.h b/tests/clar/clar_libgit2_alloc.h
new file mode 100644
index 000000000..78a18b67d
--- /dev/null
+++ b/tests/clar/clar_libgit2_alloc.h
@@ -0,0 +1,11 @@
+#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
diff --git a/tests/util/alloc.c b/tests/util/alloc.c
new file mode 100644
index 000000000..492394a52
--- /dev/null
+++ b/tests/util/alloc.c
@@ -0,0 +1,68 @@
+#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);
+}