Recommended resources
The freely available resources this lesson will direct you to.
- why is it illegal to use "goto"?www.youtube.com
- How to manage memory with malloc, calloc, realloc, and free in Cwww.youtube.com
- MEM00-C. Allocate and free memory in the same module, at the same level of abstraction | CERT Secure Codingwiki.sei.cmu.edu
- MEM12-C. Consider using a goto chain when leaving a function on error when using and releasing resources | CERT Secure Codingwiki.sei.cmu.edu
Welcome back. Last lesson, you classified C operations by the guarantees they provide and checked preconditions before an unsafe operation. Heap allocation adds another question: when a function succeeds—or fails halfway through—who owns each allocation?
Today you’ll build a small heap-allocated record with an owned name and byte buffer. You’ll give its constructor and destructor explicit contracts, then run every allocation-failure path on your Linux VM. Allow about 40 minutes, with most of that time spent reading and running the code.
Ownership is part of the interface
Imagine a descriptor for a storage I/O buffer. Its caller supplies a name and a requested capacity; the descriptor keeps its own copy of the name and allocates the buffer. A useful contract is:
- The constructor borrows the input string. It never frees or retains the caller’s pointer.
- On success, the caller receives a fully initialized descriptor and owns it.
- On failure, the caller receives no descriptor; the constructor releases anything it allocated.
- The caller releases a successful result through one destructor, rather than freeing individual fields.
The distinction between borrowed and owned pointers prevents a subtle error: a helper must not free a pointer merely because it received that pointer as an argument. CERT’s MEM00-C recommendation uses a double-free example to show what happens when a validation helper frees storage that its caller still believes it owns.
Read CERT Secure Coding’s ownership recommendation for its caller-versus-helper distinction. The examples are more important here than the particular array being validated.
Read the opening explanation beginning with the ownership risk. Then compare the code under “Noncompliant Code Example” and “Compliant Solution”: start with the failing arrangement, and continue through the compliant process_list() example before “Risk Assessment.” Focus on which function reports an error and which function frees list; this explanation identifies the mismatched levels.
“Allocate and free at the same level” does not mean the caller must directly call free() for every field. A constructor/destructor pair operates at the same object abstraction: the constructor creates a complete descriptor, and the destructor knows how to dismantle it. The caller handles the descriptor as one owned object.
Make partial construction safe
A constructor with three allocations can fail after the first, second, or third attempt. Those are three different partially constructed states. You can make them manageable by establishing a simple invariant: every pointer field is either null or points to an allocation owned by this object. Then the destructor can handle both a finished object and one still under construction.
Watch the cleanup portion of “why is it illegal to use ‘goto’?” by Low Level. Treat it as a demonstration of control flow, not a rule that every error path needs goto.
why is it illegal to use "goto"?
Low Level contrasts duplicated cleanup with a centralized error exit. Watch for the resources that have actually been acquired at each failure point.
Watch repeated cleanup as failures accumulate, then the cleanup path that releases resources in one place. In our code, a destructor will perform that release; goto will only direct failed construction to it.
CERT’s MEM12-C recommendation provides a more detailed version of the same idea. Read its introduction and “Compliant Solution (POSIX, Goto Chain),” skipping the intermediate nested-if solution and the later historical kernel excerpt. Its file-and-object example has more resources than ours, but the central question is identical: which releases are valid when this particular operation fails?
Read CERT Secure Coding’s explanation of structured error exits to see why a cleanup label can be clearer than repeated early-return cleanup.
In the introduction, read from the resource problem through the explanation of missed releases. Then go directly to “Compliant Solution (POSIX, Goto Chain).” Read from the goto-chain explanation through its code example, stopping before “Compliant Solution (copy_process() from Linux kernel).” For each failure label, identify which allocations or open files exist when execution reaches it.
A cleanup label must not jump into a state whose cleanup assumes an allocation succeeded. For this small object, initializing pointer fields to null lets one destructor handle all post-object-allocation failures.
Build the constructor and destructor
Save the following as record.c. The test_malloc and test_calloc wrappers exist only to inject failures for this experiment. In an ordinary single-threaded production version, the constructor would call malloc and calloc directly, or use a deliberately designed allocator interface.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *name;
unsigned char *bytes;
size_t capacity;
} Record;
enum {
REC_OK,
REC_INVALID,
REC_NOMEM
};
/* Test-only failure injection: 0 disables it. */
static unsigned fail_at;
static unsigned allocation_count;
static int injected_failure(void)
{
++allocation_count;
return fail_at != 0 && allocation_count == fail_at;
}
static void *test_malloc(size_t size)
{
if (injected_failure())
return NULL;
return malloc(size);
}
static void *test_calloc(size_t count, size_t size)
{
if (injected_failure())
return NULL;
return calloc(count, size);
}
void record_destroy(Record *r)
{
if (r == NULL)
return;
free(r->bytes);
free(r->name);
free(r);
}
/*
* name must point to a valid, null-terminated string.
* out must point to writable storage for a Record pointer.
* On success: *out owns a complete Record.
* On failure: *out is NULL, if out was non-NULL.
*/
int record_create(const char *name, size_t capacity, Record **out)
{
Record *r;
size_t length;
if (out == NULL)
return REC_INVALID;
*out = NULL;
if (name == NULL || capacity == 0)
return REC_INVALID;
length = strlen(name);
if (length == SIZE_MAX)
return REC_INVALID; /* length + 1 would overflow */
r = test_malloc(sizeof *r);
if (r == NULL)
return REC_NOMEM;
r->name = NULL;
r->bytes = NULL;
r->capacity = capacity;
r->name = test_malloc(length + 1);
if (r->name == NULL)
goto fail;
memcpy(r->name, name, length + 1);
r->bytes = test_calloc(capacity, sizeof *r->bytes);
if (r->bytes == NULL)
goto fail;
*out = r;
return REC_OK;
fail:
record_destroy(r);
return REC_NOMEM;
}
int main(void)
{
for (unsigned failure = 0; failure <= 3; ++failure) {
Record *r = NULL;
int status;
fail_at = failure;
allocation_count = 0;
status = record_create("queue0", 4096, &r);
printf("fail_at=%u status=%d owns_record=%s\n",
failure, status, r != NULL ? "yes" : "no");
if ((failure == 0 && (status != REC_OK || r == NULL)) ||
(failure != 0 && (status != REC_NOMEM || r != NULL))) {
record_destroy(r);
return 1;
}
if (r != NULL)
r->bytes[0] = 0x5a; /* Use only a successful result. */
record_destroy(r); /* NULL is also safe. */
}
fail_at = 0;
allocation_count = 0;
Record *invalid = NULL;
if (record_create(NULL, 16, &invalid) != REC_INVALID ||
invalid != NULL)
return 1;
return 0;
}
Compile and run:
cc -std=c11 -O0 -g -Wall -Wextra -Wpedantic record.c -o record
./record
With this test allocator, status=0 denotes success, status=1 an invalid argument, and status=2 allocation failure. The first line should own a record; each of the next three should report no owned record.
Follow the failure at the third allocation closely. At that point, r and r->name exist, while r->bytes is null. record_destroy(r) calls free(NULL) for the buffer—which is valid—then releases the copied name and the record. At the first allocation failure, there is no r to destroy, so the function returns directly. The constructor publishes *out = r only after all allocations succeed.
Two details deserve particular care in a code review:
- Size checks precede use.
length + 1provides room for the string terminator, but only after ruling out overflow.sizeof *rfollows the object’s actual type. The nonzero-capacity rule avoids giving callers an ambiguous zero-capacity buffer contract. - The destructor is safe for partial objects, not for arbitrary pointers. Null fields are safe to free; uninitialized pointer fields are not. Nor is it safe for a caller to invoke
record_destroy(r)twice on the same non-null pointer. After destruction, the caller must stop using that pointer.
The test wrappers also make an important distinction visible: this run verifies the constructor’s response to chosen failures. It does not prove that every possible misuse of its interface is safe. In particular, neither strlen nor the null checks can validate an arbitrary dangling or non-terminated name pointer.
Key takeaways
An ownership-safe constructor has a precise handoff: it either publishes one complete owned object or publishes nothing and cleans up its partial work. Initialize owned pointer fields before a later operation can fail, keep borrowed inputs separate from owned copies, and make error exits correspond to the resources actually acquired. A destructor at the object’s level of abstraction keeps both caller code and failure cleanup consistent.
Next, you’ll investigate memory mistakes with compiler warnings and sanitizers. The constructor you just built gives you a useful baseline: its normal and injected-failure paths should be understandable before a tool reports on them.