Programming for coverage part 5
The last post used custom replacement functions and allocator interfaces to simulate allocation failures for testing purposes. Those approaches work well when you control both sides of the function call, but it won’t work if we want to test handling errors originating in libraries we don’t control.
The malloc might fail because the system is out of memory or when the resource limit is reached. This can happen both when calling malloc directly, or indirectly through functions like strdup or fopen. C does not have replacement functions, but it is not uncommon to substitute the libc malloc with a different allocator like tcmalloc or jemalloc when linking or loading the program. From the jemalloc documentation:
# At load time
LD_PRELOAD=`jemalloc-config --libdir`/libjemalloc.so.`jemalloc-config --revision` app
# Link jemalloc dynamically
cc app.c -o app -L`jemalloc-config --libdir` -Wl,-rpath,`jemalloc-config --libdir` -ljemalloc `jemalloc-config --libs`
# Link jemalloc statically
cc app.c -o app `jemalloc-config --libdir`/libjemalloc.a `jemalloc-config --libs`This shows we can provide our own custom functions at link time, such as one that simulate failures. Providing the a different malloc at link time this way works without a multiple definitions error because malloc is a weak symbol. It would not work for most third party libraries as it is unusual to declare symbols weak. Still, weak symbols would not address the main hurdle; we want to simulate failures, but some calls should still succeed. Going back to our custom new from part 4:
void* operator new(std::size_t sz) {
if (!remaining) throw std::bad_alloc{};
remaining -= 1;
if (sz == 0)
++sz;
if (void *ptr = std::malloc(sz))
return ptr;
throw std::bad_alloc{};
}In our replacement new we use malloc to get memory from the OS. If we similarly try to replace malloc we would get this, which clearly cannot work as the recursive malloc calls would run infinitely (or probably crash stack overflow and pretty quickly). So how do we make this work without having to implement a complete allocator?
void *malloc(size_t sz) {
if (!remaining)
return NULL;
remaining -= 1;
return malloc(sz);
}Some linkers (GNU ld, GNU gold, lld, maybe more) supports the
--wrap=<symbol> flag, which replaces calls to symbol with calls to
__wrap_symbol while keeping the original definition of symbol
available as __real_symbol.
void *__real_malloc(size_t);
void *__wrap_malloc(size_t sz) {
if (!remaining)
return NULL;
remaining -= 1;
return __real_malloc(sz);
}Here is an example test. This is a custom strdup and a test that expects strdup to fail the second time it is called. Memory allocation failures are quite rare, so this test is unlikely to pass.
#include <assert.h>
#include <stdlib.h>
#include <string.h>
char *xstrdup(const char *s) {
size_t len = strlen(s) + 1;
void *new = malloc(len);
if (new == NULL)
return NULL;
return (char *) memcpy(new, s, len);
}
void check() {
const char *refstr = "The quick brown fox jumps over the lazy dog";
assert(xstrdup(refstr) != NULL);
assert(xstrdup(refstr) == NULL);
}
int main() {
check();
}$ gcc xstrdup.c -o xstrdup
$ ./xstrdup
xstrdup: xstrdup.c:16: check: Assertion `xstrdup(refstr) == NULL' failed.Now that we have confirmed that our test fails as expected we can make it pass. We add a new file alloc.c with our wrapped malloc, and include a print for good measure so it is easier to see what is going on.
#include <stdio.h>
static size_t remaining = (size_t)-1;
void fail_nth_malloc(size_t n) { remaining = n; }
void *__real_malloc(size_t);
void *__wrap_malloc(size_t sz) {
printf ("malloc(%zu), %zu remaining\n", sz, remaining);
if (!remaining)
return NULL;
remaining -= 1;
return __real_malloc(sz);
}We must also instruct our test to fail the next malloc by calling fail_nth_malloc().
void fail_nth_malloc(size_t);
void check() {
const char *refstr = "The quick brown fox jumps over the lazy dog";
assert(xstrdup(refstr) != NULL);
// Fail next malloc
fail_nth_malloc(0);
assert(xstrdup(refstr) == NULL);
}$ gcc xstrdup-2.c alloc.c -o xstrdup-2 -Wl,--wrap=malloc
$ ./xstrdup-2
malloc(44), 18446744073709551615 remaining
malloc(44), 0 remainingUsing function wrapping to simulate failures is less ergonomic than replacement new and dynamic allocators, and should maybe be used as a last resort. The wrapping is done by the linker and has to be wired up outside the programming language and, because of their global nature, require small and focused test binaries to not interfere with other tests. Still, it a quite useful feature that can support complex failure simulation sourced from libraries and we can’t (or won’t) change the design. This post used malloc as an example, but function wrapping works well for arbitrary functions.
More resources: