Can mulle-allocator Be Used in Signal Handlers or with setjmp/longjmp?

No, mulle-allocator cannot be used safely in signal handlers with the default configuration because it relies on non-async-signal-safe malloc/realloc, but it works fine with setjmp/longjmp and can be made signal-safe with a custom allocator.

The mulle-c/mulle-allocator library provides a thin, extensible wrapper around C memory allocation functions. While the default implementation forwards calls to the standard library, its architecture allows you to substitute async-signal-safe alternatives for use in constrained environments like signal handlers.

Why the Default mulle-allocator Fails in Signal Handlers

The default allocator (mulle_stdlib_allocator) is not async-signal-safe because it delegates directly to malloc, realloc, and free. According to POSIX, these standard library functions are not safe to call from within a signal handler due to potential deadlocks if the signal interrupts an ongoing allocation.

In src/mulle-allocator.c (lines 101–108), the core allocation wrapper demonstrates this dependency:

static inline void *_mulle_allocator_malloc( struct mulle_allocator *p,
                                            size_t size)
{
    void *q = (*p->realloc)( NULL, size, p );   // ← uses realloc()
    if (MULLE_C_UNLIKELY(!q))
        (*p->fail)( p, NULL, size );           // ← abort()
    return q;
}

Because this code path invokes realloc (and by extension malloc), using mulle_malloc or mulle_allocator_malloc with the default allocator inside a signal handler invokes undefined behavior. The fail callback further compounds this issue by calling abort(), which is also not async-signal-safe, as evidenced in test/fails/fail-malloc-abort.c.

Using mulle-allocator with setjmp/longjmp

Unlike signal handlers, setjmp/longjmp impose no special restrictions on mulle-allocator. The library maintains no hidden global state; it operates solely on the function pointers stored within the mulle_allocator struct passed to each call.

When you call mulle_allocator_malloc before a setjmp and again after a longjmp, the allocator functions normally. The only consideration is standard C memory management: any pointer allocated before the jump but not freed becomes a leak, as longjmp unwinds the stack without calling destructors.

#include <setjmp.h>
#include <mulle-allocator/mulle-allocator.h>

jmp_buf env;

void risky(void)
{
    char *p = mulle_malloc(128);   // allocate
    /* … something that may longjmp … */
    longjmp(env, 1);               // unwind stack, p is leaked (as always)
}

int main(void)
{
    if (setjmp(env) == 0) {
        risky();                   // first entry
    } else {
        /* after longjmp – we can still allocate */
        char *q = mulle_malloc(64);
        mulle_free(q);
    }
    return 0;
}

Making mulle-allocator Safe for Signal Handlers

To use mulle-allocator inside a signal handler, you must provide a custom allocator struct that uses only async-signal-safe operations. The library's design explicitly supports this through its user-supplied function pointers.

A typical approach implements a lock-free bump allocator using a static buffer. This avoids malloc entirely and uses only signal-safe operations like accessing static storage and arithmetic.

#include <mulle-allocator/mulle-allocator.h>
#include <signal.h>

/* ----- custom bump allocator ----- */
static void *sigalloc_realloc(void *ptr, size_t size,
                              struct mulle_allocator *a)
{
    (void)ptr; (void)a;
    static unsigned char buf[1024];
    static size_t pos;
    if (!size) return NULL;               // mimics free behavior
    if (pos + size > sizeof buf)          // out of space → fail
        return NULL;
    void *p = buf + pos;
    pos += size;
    return p;
}

static struct mulle_allocator sig_allocator = {
    .calloc  = NULL,
    .realloc = sigalloc_realloc,
    .free    = NULL,                      // nothing to free in bump allocator
    .fail    = mulle_allocation_fail,
    .abafree = mulle_allocator_no_aba_abort,
    .aba     = NULL
};

/* ----- signal handler ----- */
static void sig_handler(int sig)
{
    (void)sig;
    /* safe allocation inside a signal handler */
    char *msg = mulle_allocator_malloc(&sig_allocator, 32);
    if (msg) {
        msg[0] = 'H'; msg[1] = 'i'; msg[2] = '\0';
        /* cannot free – the allocator never frees */
    }
    _exit(0);        // async-signal-safe termination
}

int main(void)
{
    struct sigaction sa = { .sa_handler = sig_handler };
    sigaction(SIGUSR1, &sa, NULL);
    pause();
    return 0;
}

The global allocator instances defined in src/mulle-allocator.h—such as mulle_allocator_default, mulle_allocator_stdlib, and mulle_allocator_stdlib_nofree—all use the standard library and are therefore unsafe for signal handlers. You must initialize your own struct mulle_allocator with signal-safe function pointers as shown above.

Summary

  • Signal handlers: The default mulle-allocator is unsafe because src/mulle-allocator.c implements _mulle_allocator_malloc using non-async-signal-safe realloc and abort calls.
  • setjmp/longjmp: Fully supported; the allocator is stateless and works across jumps, though you must manage memory leaks manually.
  • Customization: You can create signal-safe allocators by providing custom realloc, free, and fail function pointers that use only async-signal-safe operations like static buffer management.
  • Key files: src/mulle-allocator.c contains the core wrapper logic, while src/mulle-allocator.h defines the public API and global allocator instances.

Frequently Asked Questions

Is malloc async-signal-safe?

No. POSIX explicitly lists malloc, realloc, and free as functions that are not async-signal-safe. If a signal interrupts a thread currently executing inside malloc, calling malloc again in the handler can cause a deadlock due to internal locking.

Can I use mulle_free in a signal handler?

Only if the allocator was configured with a signal-safe free implementation. The default mulle_free calls the standard free, which is not async-signal-safe. If you use a custom bump allocator that sets the free pointer to NULL or a no-op function, you avoid this risk, though you lose the ability to reclaim memory.

What happens to allocations after longjmp?

Any memory allocated via mulle_allocator_malloc before a longjmp remains allocated after the jump, but the pointer variable holding the address may be invalid if it was a local automatic variable in the unwound stack frame. This is standard C behavior; mulle-allocator does not add any special cleanup or tracking, so you must free such memory before the jump or accept the leak.

How do I create a signal-safe allocator?

Define a struct mulle_allocator with function pointers that use only async-signal-safe operations. Replace realloc with a bump allocator or mmap-based allocator that avoids locks, set free to NULL if you do not need deallocation, and ensure the fail callback uses _exit rather than abort. Pass this custom struct to mulle_allocator_malloc instead of using the default global instances.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →