C / DYNAMIC MEMORY
Cleanup patterns: goto fail versus nested free chains
Rewrite a multi-allocation C function as a flat goto ladder so every failure path releases exactly what it acquired, with one free call per resource.
What you will learn
- Rewrite a nested free chain as a flat ladder with one release call per resource
- Order cleanup labels in reverse of acquisition so fall-through releases the right set
- Initialize every handle to its failure sentinel before the first goto
- Hand off ownership by nulling locals so one cleanup block serves success and failure
Understanding Cleanup patterns: goto fail versus nested free chains
A constructor that acquires three things has three failure points, and each one owes the program a different amount of cleanup: fail at the first and nothing needs releasing, fail at the third and two blocks are already live. Writing that as a nested chain of if statements works, but the successful case ends up buried at the innermost level and each free sits far from the malloc it matches. The flat variant, where every error branch carries its own cleanup block, is worse: n acquisitions produce n(n-1)/2 free calls, and inserting a resource in the middle means adding a line to every error path below it.
The ladder replaces all of that with one release site per resource. Each acquisition failure jumps to the label that releases the previously acquired resource, and each label falls through into the next, so a label list written in reverse acquisition order produces exactly the right suffix of the release sequence for every jump target. That fall-through is the whole trick, and it is why the labels must be reversed rather than listed in the order the resources were taken. One constraint comes from the language rather than the pattern: a goto can jump past a declaration that has an initializer while the variable stays in scope, so any handle a label touches must be declared and given a value before the first jump.
The alternative shape is a single cleanup label reached by both the success and the failure paths, with every handle initialized to its failure sentinel at the top. It works because free on a null pointer is defined to do nothing, so the shared block can release unconditionally; the success path stores the pointers where the caller can reach them and then sets its own locals to NULL, turning those frees into no-ops. Resources whose sentinel is not a null pointer need an explicit guard, which is why a descriptor starts at -1 and the cleanup reads if (fd >= 0) close(fd). Reach for the ladder when each resource has its own release call, and for the single exit when there are enough handles that inventing a label per resource stops paying for itself.
<stdio.h>
<stdlib.h>
<string.h>
static int budget; /* allocations still permitted */
static int live; /* blocks currently outstanding */
static void *xmalloc(size_t n, const char *tag)
{
void *p = (budget > 0) ? malloc(n) : NULL;
if (p) {
budget--;
live++;
printf(" malloc %s -> ok\n", tag);
} else {
printf(" malloc %s -> FAILED\n", tag);
}
return p;
}
static void xfree(void *p, const char *tag)
{
if (p) {
live--;
printf(" free %s\n", tag);
}
free(p);
}
struct record {
char *name;
int *values;
};
static struct record *record_new(const char *name, size_t n)
{
struct record *r = xmalloc(sizeof *r, "record");
if (!r)
return NULL; /* nothing acquired yet */
r->name = xmalloc(strlen(name) + 1, "name");
if (!r->name)
goto err_free_record;
strcpy(r->name, name);
r->values = xmalloc(n * sizeof *r->values, "values");
if (!r->values)
goto err_free_name;
return r;
err_free_name:
xfree(r->name, "name");
err_free_record:
xfree(r, "record");
return NULL;
}
int main(void)
{
for (int allowed = 0; allowed <= 3; allowed++) {
budget = allowed;
printf("budget %d\n", allowed);
struct record *r = record_new("kappa", 4);
if (r) {
printf(" built %s\n", r->name);
xfree(r->values, "values");
xfree(r->name, "name");
xfree(r, "record");
}
printf(" outstanding %d\n", live);
}
return 0;
}
Every failure path owes exactly the releases for the resources acquired before it, and reverse-ordered fall-through labels express all of those paths with one release call per resource.
Worked examples
One exit for both paths
Shows a single cleanup label shared by success and failure, made safe by null sentinels and by nulling the locals once ownership moves to the caller.
<stdio.h>
<stdlib.h>
struct pair { int *a; int *b; };
/* 0 on success; on failure *out is untouched and nothing leaks. */
static int pair_init(struct pair *out, size_t n, int step_ok)
{
int *a = NULL, *b = NULL;
int rc = -1;
a = malloc(n * sizeof *a);
if (!a)
goto cleanup;
if (!step_ok) {
printf("second step refused\n");
goto cleanup;
}
b = malloc(n * sizeof *b);
if (!b)
goto cleanup;
out->a = a;
out->b = b;
a = b = NULL; /* ownership handed to the caller */
rc = 0;
cleanup:
free(a); /* both are NULL on the success path */
free(b);
return rc;
}
int main(void)
{
struct pair p = { NULL, NULL };
int rc;
rc = pair_init(&p, 8, 0);
printf("rc=%d p.a=%s\n", rc, p.a ? "set" : "NULL");
rc = pair_init(&p, 8, 1);
printf("rc=%d p.a=%s p.b=%s\n", rc,
p.a ? "set" : "NULL", p.b ? "set" : "NULL");
free(p.a);
free(p.b);
return 0;
}
Example explained
Line 1a and b are declared and set to NULL above every goto, so the shared cleanup always reads a defined value.
Line 2The refused step jumps with only a live, and free(b) on a null b does nothing, so one block of frees covers both failures.
Line 3a = b = NULL after the pointers are stored in *out is what keeps the blocks alive past the cleanup label.
Line 4rc starts at -1 and is set to 0 only after the last step, so any path that jumps early reports failure by construction.
A handle whose sentinel is not NULL
Shows the same ladder over two resources with different release calls, where the failure value is -1 and the release must be guarded by a comparison.
<stdio.h>
<stdlib.h>
static int slots[2]; /* 0 = free, 1 = taken */
static int slot_acquire(void)
{
for (int i = 0; i < 2; i++)
if (!slots[i]) {
slots[i] = 1;
return i; /* a handle, not a pointer */
}
return -1; /* the failure sentinel */
}
static void slot_release(int h)
{
if (h >= 0)
slots[h] = 0;
}
static char *make(size_t cap)
{
int h = slot_acquire();
if (h < 0)
return NULL;
char *buf = malloc(cap);
if (!buf)
goto err_release;
if (cap < 4) /* a later step fails */
goto err_free;
snprintf(buf, cap, "slot %d", h);
slot_release(h); /* released on success too */
return buf;
err_free:
free(buf);
err_release:
slot_release(h);
return NULL;
}
int main(void)
{
char *a = make(16);
char *b = make(2);
printf("a = %s\n", a ? a : "(failed)");
printf("b = %s\n", b ? b : "(failed)");
printf("slots busy: %d\n", slots[0] + slots[1]);
free(a);
free(b);
return 0;
}
Example explained
Line 1slot_acquire reports failure with -1, so the guard inside slot_release is a >= 0 test and the free(NULL) shortcut does not apply.
Line 2The first failure returns directly instead of jumping, because at that point nothing has been acquired to release.
Line 3goto err_free frees the buffer and then falls into err_release, which is the reverse of the order the two resources were taken.
Line 4slot_release is also called just before the success return, since the slot is only borrowed while buf is what the caller keeps.
Important notes
The name goto fail comes from Apple's 2014 TLS bug, where a duplicated unconditional goto fail under a braceless if skipped the signature check and the function still returned success. The fix is to brace the guard and make sure the cleanup path returns failure, not to avoid unwind labels.
A ladder must release only what this function acquired; freeing a buffer the caller passed in looks symmetric but leaves the caller holding a pointer to freed memory.
Common mistakes
Listing the labels in acquisition order, so an early failure falls through into releases for resources it never took and calls free on an indeterminate pointer.
Omitting the return above the first label, so the success path falls into the ladder and frees the object it is about to return, leaving the caller with a dangling pointer.
Declaring a handle in the middle of the function and jumping past its initializer; the variable is in scope at the label but was never set, so the cleanup frees garbage.
Try it yourself
Change, predict, then run
Extend record_new from the main example with a fourth allocation, a heap copy of a tag string, placed between name and values. Add the one label and one retargeted jump it needs, then run the loop with budget 0 through 4 and confirm outstanding prints 0 for every budget.
Open the C workspaceCheck your understanding
A function ends with a single cleanup label containing free(a); free(b); return rc; and control reaches that label on both the success and the failure paths. For the success path to leave the two blocks alive for the caller, what must it do first?
- Return before the label so the cleanup block is never reached on success
- Set the local pointers a and b to NULL after storing them where the caller can reach them
- Nothing, because free leaves a block alone while another pointer still refers to it
- Free the locals and allocate fresh copies for the caller before falling through
Show answer
Nulling the locals turns the shared free calls into free(NULL), which is defined to do nothing, so the blocks survive and one cleanup block serves both paths. Returning early is a real technique but it is the ladder pattern, not this one: it abandons the single shared exit the function was built around. Option three is the dangerous belief, since free tracks no references at all and would hand the caller freed memory.