The device boots perfectly. Days later it crashes. Not because of a spectacular bug, but because dozens of tiny allocations quietly accumulated over time.
When embedded developers hear the term memory leak, the first thought is often “someone forgot to call free().” While this is technically true, it barely scratches the surface of the problem.
A missing free() could happen because the developer simply forgot it, but there are other reasons that are quite more complex. It can be the result of a program losing track of memory that it is still responsible for. In other words, it is fundamentally an ownership problem.
This distinction is important because understanding why memory leaks happen makes them easier to recognize, reason about, and ultimately avoid.
In embedded systems, where RAM is often measured in kilobytes or a few megabytes, even a small leak can eventually bring down an otherwise stable application. A device that runs continuously for weeks or months has very little tolerance for memory that is gradually disappearing.
In this article, we’ll look at what memory leaks really are, why they happen, and why embedded software is particularly vulnerable to them.
What Is a Memory Leak?#
A memory leak occurs when dynamically allocated memory remains allocated after it is no longer needed.
For example:
while (1) {
char *buffer = malloc(256);
process(buffer);
/* forgot free(buffer) */
}The 256-bytes buffer remains allocated, and since this happens in an infinite loop, this allocated memory grows and the available heap gradually shrinks. This keeps happening until the memory manager (like malloc) fails to satisfy subsequent memory allocation requests, resulting in system crashes.
Unlike memory corruption, which often causes immediate and unpredictable failures due to unintentional writes, memory leaks are usually silent. They accumulate over time, gradually exhausting the available heap until the application begins to fail, often long after the code that introduced the leak has executed.
Common Red Flags That Can Lead To Memory Leaks#
Memory Ownership#
Memory ownership means that every dynamically allocated block of memory should have one clearly defined owner. The owner’s responsibility is simple: Release the memory when it is no longer needed.
Without a clearly defined owner, it becomes unclear who is responsible for freeing the allocation, or whether anyone will free it at all.
Ownership through API contracts#
Ownership should be part of every API’s contract.
Consider this function:
void process_buffer(char *buffer);Does this function take a pointer to an already allocated memory ? Does it free the buffer after processing it ? Can the function keep the pointer in its book keeping after returning for future usage?
Unless these questions are answered explicitly, different developers may make different assumptions. One assumes the caller will free the buffer; another assumes the callee will. The result is either a memory leak or a double free.
Good APIs document ownership just as clearly as they document parameters and return values.
Losing ownership#
Memory leaks often occur because the pointer, not the memory, is lost.
For example:
char *ptr = malloc(128);
// some code
ptr = get_default_buffer();The original allocation still exists, but nothing points to it anymore. Since its address has been lost, it can never be released.
The same problem can happen through pointer arithmetic.
char *ptr = malloc(128);
ptr++;The original address returned by malloc() has been modified. Calling
free(ptr);is now invalid because ptr no longer points to the beginning of the allocated block.
While pointer arithmetic itself is perfectly valid, performing it directly on the owning pointer is often dangerous.
One technique that helps prevent accidental pointer modification is using a const pointer.
char * const ptr = malloc(128);Here, the contents of the memory can still be modified, but the pointer itself cannot be reassigned or incremented accidentally.
This doesn’t eliminate memory leaks, but it removes one surprisingly common way of losing ownership.
Partially Freeing Complex Structures#
Consider a structure like:
struct complex_struct {
int val;
double another_val;
char* buf;
};Now, you have a dynamically allocated instance of this structure:
struct complex_struct *cs = get_complex_struct();If get_complex_struct allocates buf as well, and you are done with cs, then you only free(cs), you are letting behind the allocated memory, pointed to by cs->buf.
Developers should be very careful about this situation, which can even get more complex by having nested structures. In this case, you should first free(cs->buf), then free(cs)
Interrupted control flow#
Modern embedded software often combines C and C++.
This introduces another source of leaks: exceptions.
Consider:
char *buffer = (char *)malloc(256);
cpp_function();
free(buffer);If cpp_function() throws an exception, execution immediately leaves the function.
The call to free() is skipped entirely, resulting in memory leaks.
Forgotten cleanup#
The simplest case is forgetting to release memory.
char *buffer = malloc(256);
if (initialize(buffer) != 0)
return ERROR;
/* ... */
free(buffer);If the function returns early, free() is never reached.
As software evolves, new error paths are added, additional return statements appear, or complex if/else blocks are introduced. Memory cleanup logic is easily overlooked.
Safety standards such as MISRA-C encourage keeping functions simple and limiting control-flow complexity. Simpler control flow makes cleanup paths easier to reason about and reduces the likelihood of overlooking resource release.
Why Embedded Systems Care More#
Desktop applications often terminate and restart regularly. Any leaked memory is eventually reclaimed when the process exits.
Embedded systems are different, where devices are expected to run continuously for weeks, months, or even years. Every leaked allocation permanently reduces the available heap. This accumulates over time until we run out of memory, and crashes happen.
Conclusion#
Memory leaks are often the consequence of broken ownership.
Whether caused by unclear API contracts, pointer manipulation, or human mistakes like forgotten cleanup or exceptions crossing C/C++ boundaries, the underlying issue is the same: the program has lost responsibility for memory it allocated.
Once ownership becomes an explicit part of software design, not just implementation detail, memory leaks become much easier to understand and far less likely to appear.
Reply by Email