Browse files

Implement static allocator for initialization

The previous interposition logic based on plain usage of `dlsym` analogously to various online examples led to a deadlock during _neovim_ startup. This deadlock was caused by _neovim_'s custom memory allocation library _jemalloc_ because it calls `mmap` during its initialization phase. The problem with calling `mmap` during initialization is that this already leads to executing `libChangeLog`'s `mmap` version whoes static `actual_mmap` function pointer is not initialized at this point in time. This is detected and leads to a call to `dlsym` to remedy this situation. Sadly `dlsym` in turn requires memory allocation using `calloc` which leads us back to initializing _jemalloc_ and as such to a deadlock.

I first saw this as a bug in _jemalloc_ which seemed to be confirmed by a short search in my search engine of choice. This prompted me to create an appropriate [bug report](jemalloc/jemalloc#329) which was dismissed as a problem in the way `mmap` was interposed and not as a bug in the library. Thus it seems to be accepted practice that it is not the responsibility of a custom memory allocator to cater to the initialization needs of other libraries relying on function interposition. This is of course a valid position as the whole issue is a kind of _chicken and egg_ problem where both sides can be argued.

To cut to the chase I was left with the only option of working around this deadlock by adapting `libChangeLog` to call `dlsym` without relying on the wrapped application's memory allocator of choice. The most straight forward way to do this is to provide another custom memory allocator alongside the _payload_ function interpositions of `mmap` and friends.

`init/` implements such a selectively transparent memory allocator that offers a small static buffer for usage in the context of executing `dlsym`.The choice between forwarding memory allocation requests to the wrapped application's allocator and using the static buffer is governed by `init::dlsymContext`. This tiny helper class maintains an `dlsym_level` counter by posing as a scope guard.

The end result of this extension to `libChangeLog` is that it now also works with applications using _jemalloc_ such as _neovim_ and should overall be much more robust during its initialization phase.
  • Loading branch information...
KnairdA committed Feb 17, 2016
1 parent 98f68dd commit af756d78ac042a2eed2417c5250d4b675d43bf93
Showing with 123 additions and 5 deletions.
  1. +1 −0 CMakeLists.txt
  2. +7 −3 src/actual.h
  3. +43 −2 src/
  4. +50 −0 src/init/
  5. +22 −0 src/init/alloc.h
@@ -14,6 +14,7 @@ add_library(
@@ -1,24 +1,28 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#include <dlfcn.h>
#include <sys/mman.h>
#include <sys/uio.h>
#include <memory>
#include <cstring>
#include "init/alloc.h"
namespace actual {
template <class Result, typename... Arguments>
using ptr = Result(*)(Arguments...);
template <typename FunctionPtr>
FunctionPtr get_ptr(const std::string& symbol_name) {
init::dlsymContext guard;
const void* symbol_address{ dlsym(RTLD_NEXT, symbol_name.c_str()) };
FunctionPtr actual_function{};
@@ -1,5 +1,6 @@
#include "actual.h"
#include "init/alloc.h"
#include "utility/io.h"
#include "utility/logger.h"
#include "tracking/path_matcher.h"
@@ -14,8 +15,8 @@ static std::unique_ptr<utility::Logger> logger;
static std::unique_ptr<tracking::PathMatcher> matcher;
static std::unique_ptr<tracking::ChangeTracker> tracker;
void init() __attribute__ ((constructor));
void init() {
void initialize() __attribute__ ((constructor));
void initialize() {
if ( getenv("CHANGE_LOG_TARGET") != NULL ) {
fd_guard = std::make_unique<utility::FileDescriptorGuard>(
@@ -81,6 +82,46 @@ inline void track_remove(const std::string& path) {
void free(void* ptr) {
static actual::ptr<void, void*> actual_free{};
if ( !actual_free ) {
actual_free = actual::get_ptr<decltype(actual_free)>("free");
if ( !init::from_static_buffer(ptr) ) {
void* malloc(size_t size) {
static actual::ptr<void*, size_t> actual_malloc{};
if ( init::dlsymContext::is_active() ) {
return init::static_malloc(size);
} else {
if ( !actual_malloc ) {
actual_malloc = actual::get_ptr<decltype(actual_malloc)>("malloc");
return actual_malloc(size);
void* calloc(size_t block, size_t size) {
static actual::ptr<void*, size_t, size_t> actual_calloc{};
if ( init::dlsymContext::is_active() ) {
return init::static_calloc(block, size);
} else {
if ( !actual_calloc ) {
actual_calloc = actual::get_ptr<decltype(actual_calloc)>("calloc");
return actual_calloc(block, size);
ssize_t write(int fd, const void* buffer, size_t count) {
static actual::ptr<ssize_t, int, const void*, size_t> actual_write{};
@@ -0,0 +1,50 @@
#include "alloc.h"
#include <cstdio>
#include <cstdint>
namespace init {
std::uint8_t dlsym_level = 0;
size_t buffer_offset = 0;
char buffer[4096];
void* static_malloc(size_t size) {
if ( buffer_offset + size >= sizeof(buffer) ) {
std::fprintf(stderr, "static allocator out of memory: %lu\n", buffer_offset);
buffer_offset += size;
return buffer + buffer_offset;
void* static_calloc(size_t block, size_t size) {
void* const mem = static_malloc(block * size);
for ( size_t i = 0; i < (block * size); ++i ) {
*(static_cast<char* const>(mem) + i) = '\0';
return mem;
bool from_static_buffer(void* ptr) {
return static_cast<char*>(ptr) >= buffer
&& static_cast<char*>(ptr) <= buffer + sizeof(buffer);
dlsymContext::dlsymContext() {
dlsymContext::~dlsymContext() {
bool dlsymContext::is_active() {
return dlsym_level > 0;
@@ -0,0 +1,22 @@
#include <cstdlib>
namespace init {
void* static_malloc(size_t size);
void* static_calloc(size_t block, size_t size);
bool from_static_buffer(void* ptr);
struct dlsymContext {
static bool is_active();

0 comments on commit af756d7

Please sign in to comment.