Skip to content

Commit 90cc077

Browse files
pks-tethomson
authored andcommitted
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.
1 parent 9dd1bfe commit 90cc077

File tree

4 files changed

+200
-2
lines changed

4 files changed

+200
-2
lines changed

src/libgit2/threadstate.c

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,23 @@ git_threadstate *git_threadstate_get(void)
7575
if ((threadstate = git_tlsdata_get(tls_key)) != NULL)
7676
return threadstate;
7777

78-
if ((threadstate = git__calloc(1, sizeof(git_threadstate))) == NULL ||
79-
git_str_init(&threadstate->error_buf, 0) < 0)
78+
/*
79+
* Avoid git__malloc here, since if it fails, it sets an error
80+
* message, which requires thread state, which would allocate
81+
* here, which would fail, which would set an error message...
82+
*/
83+
84+
if ((threadstate = git__allocator.gmalloc(sizeof(git_threadstate),
85+
__FILE__, __LINE__)) == NULL)
8086
return NULL;
8187

88+
memset(threadstate, 0, sizeof(git_threadstate));
89+
90+
if (git_str_init(&threadstate->error_buf, 0) < 0) {
91+
git__allocator.gfree(threadstate);
92+
return NULL;
93+
}
94+
8295
git_tlsdata_set(tls_key, threadstate);
8396
return threadstate;
8497
}

tests/clar/clar_libgit2_alloc.c

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright (C) the libgit2 contributors. All rights reserved.
3+
*
4+
* This file is part of libgit2, distributed under the GNU GPL v2 with
5+
* a Linking Exception. For full terms see the included COPYING file.
6+
*/
7+
8+
#include "clar_libgit2_alloc.h"
9+
10+
static size_t bytes_available;
11+
12+
/*
13+
* The clar allocator uses a tagging mechanism for pointers that
14+
* prepends the actual pointer's number bytes as `size_t`.
15+
*
16+
* First, this is required in order to be able to implement
17+
* proper bookkeeping of allocated bytes in both `free` and
18+
* `realloc`.
19+
*
20+
* Second, it may also be able to spot bugs that are
21+
* otherwise hard to grasp, as the returned pointer cannot be
22+
* free'd directly via free(3P). Instead, one is forced to use
23+
* the tandem of `cl__malloc` and `cl__free`, as otherwise the
24+
* code is going to crash hard. This is considered to be a
25+
* feature, as it helps e.g. in finding cases where by accident
26+
* malloc(3P) and free(3P) were used instead of git__malloc and
27+
* git__free, respectively.
28+
*
29+
* The downside is obviously that each allocation grows by
30+
* sizeof(size_t) bytes. As the allocator is for testing purposes
31+
* only, this tradeoff is considered to be perfectly fine,
32+
* though.
33+
*/
34+
35+
static void *cl__malloc(size_t len, const char *file, int line)
36+
{
37+
char *ptr = NULL;
38+
size_t alloclen;
39+
40+
GIT_UNUSED(file);
41+
GIT_UNUSED(line);
42+
43+
if (len > bytes_available)
44+
goto out;
45+
46+
if (GIT_ADD_SIZET_OVERFLOW(&alloclen, len, sizeof(size_t)) ||
47+
(ptr = malloc(alloclen)) == NULL)
48+
goto out;
49+
memcpy(ptr, &len, sizeof(size_t));
50+
51+
bytes_available -= len;
52+
53+
out:
54+
return ptr ? ptr + sizeof(size_t) : NULL;
55+
}
56+
57+
static void cl__free(void *ptr)
58+
{
59+
if (ptr) {
60+
char *p = ptr;
61+
size_t len;
62+
memcpy(&len, p - sizeof(size_t), sizeof(size_t));
63+
free(p - sizeof(size_t));
64+
bytes_available += len;
65+
}
66+
}
67+
68+
static void *cl__realloc(void *ptr, size_t size, const char *file, int line)
69+
{
70+
size_t copybytes = 0;
71+
char *p = ptr;
72+
void *new;
73+
74+
if (p)
75+
memcpy(&copybytes, p - sizeof(size_t), sizeof(size_t));
76+
if (copybytes > size)
77+
copybytes = size;
78+
79+
if ((new = cl__malloc(size, file, line)) == NULL)
80+
goto out;
81+
memcpy(new, p, copybytes);
82+
cl__free(p);
83+
84+
out:
85+
return new;
86+
}
87+
88+
void cl_alloc_limit(size_t bytes)
89+
{
90+
git_allocator alloc;
91+
92+
alloc.gmalloc = cl__malloc;
93+
alloc.grealloc = cl__realloc;
94+
alloc.gfree = cl__free;
95+
96+
git_allocator_setup(&alloc);
97+
98+
bytes_available = bytes;
99+
}
100+
101+
void cl_alloc_reset(void)
102+
{
103+
git_allocator stdalloc;
104+
git_stdalloc_init_allocator(&stdalloc);
105+
git_allocator_setup(&stdalloc);
106+
}

tests/clar/clar_libgit2_alloc.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#ifndef __CLAR_LIBGIT2_ALLOC__
2+
#define __CLAR_LIBGIT2_ALLOC__
3+
4+
#include "clar.h"
5+
#include "common.h"
6+
#include "git2/sys/alloc.h"
7+
8+
void cl_alloc_limit(size_t bytes);
9+
void cl_alloc_reset(void);
10+
11+
#endif

tests/util/alloc.c

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#include "clar_libgit2.h"
2+
#include "clar_libgit2_alloc.h"
3+
#include "alloc.h"
4+
5+
void test_alloc__cleanup(void)
6+
{
7+
cl_alloc_reset();
8+
}
9+
10+
void test_alloc__oom(void)
11+
{
12+
void *ptr = NULL;
13+
14+
cl_alloc_limit(0);
15+
16+
cl_assert(git__malloc(1) == NULL);
17+
cl_assert(git__calloc(1, 1) == NULL);
18+
cl_assert(git__realloc(ptr, 1) == NULL);
19+
cl_assert(git__strdup("test") == NULL);
20+
cl_assert(git__strndup("test", 4) == NULL);
21+
}
22+
23+
void test_alloc__single_byte_is_exhausted(void)
24+
{
25+
void *ptr;
26+
27+
cl_alloc_limit(1);
28+
29+
cl_assert(ptr = git__malloc(1));
30+
cl_assert(git__malloc(1) == NULL);
31+
git__free(ptr);
32+
}
33+
34+
void test_alloc__free_replenishes_byte(void)
35+
{
36+
void *ptr;
37+
38+
cl_alloc_limit(1);
39+
40+
cl_assert(ptr = git__malloc(1));
41+
cl_assert(git__malloc(1) == NULL);
42+
git__free(ptr);
43+
cl_assert(ptr = git__malloc(1));
44+
git__free(ptr);
45+
}
46+
47+
void test_alloc__realloc(void)
48+
{
49+
char *ptr = NULL;
50+
51+
cl_alloc_limit(3);
52+
53+
cl_assert(ptr = git__realloc(ptr, 1));
54+
*ptr = 'x';
55+
56+
cl_assert(ptr = git__realloc(ptr, 1));
57+
cl_assert_equal_i(*ptr, 'x');
58+
59+
cl_assert(ptr = git__realloc(ptr, 2));
60+
cl_assert_equal_i(*ptr, 'x');
61+
62+
cl_assert(git__realloc(ptr, 2) == NULL);
63+
64+
cl_assert(ptr = git__realloc(ptr, 1));
65+
cl_assert_equal_i(*ptr, 'x');
66+
67+
git__free(ptr);
68+
}

0 commit comments

Comments
 (0)