Common Heap Management Errors

Freeing a pointer not obtained from malloc

Due to performance trade-offs in the way in which malloc metadata is maintained, it is not always easy for non-hardened implementations of free to verify that a pointer it receives is valid before re-inserting the associated memory block into the freestore. For example, a programmer could attempt to free an address somewhere within the heap but not at the start of a valid malloc memory block, e.g., by trying to free an address pointing to the middle of an array, or by incrementing a previously-valid malloc-obtained pointer before freeing it. In another scenario, a pointer could point outside the heap entirely, e.g., such as a pointer to a local or global variable. While this may sound silly, one plausible example is when a programmer calls potentially-confusing functions like Windows’ FormatMessage, which sometimes allocates heap memory for an error message string and at other times copies the message to a programmer-supplied buffer, depending on which flags are passed to it.

Because free implementations must reconstruct the locations of important metadata relating to a chunk of dynamic memory using constant offsets from the pointer passed to free, non-malicious instances of this error generally lead to an attempt by free to dereference an invalid pointer and therefore an immediate crash. Even sources developed long before heap corruption was understood to be a security issue warn about this mistake, including Kernighan and Ritchie’s 1988 advice in The C Programming Language that, “it is a ghastly error to free something not obtained by calling calloc or malloc.” Of course, it is now known that under the right circumstances, an attacker can overwrite selected memory locations by placing maliciously-crafted fake malloc metadata beyond an invalid memory location before it is passed to free, such as in the well-known “unlink” attack against glibc.

Let’s look at a trivial example of freeing a non-heap pointer given in llist.c and shown below. In this case, head is a dummy linked list node, a common technique used when implementing a linked list to avoid special-case code when inserting or deleting at the front of the list. Unlike every other node in the list which the code allocates by calling malloc, the head node is a local variable and therefore allocated on the stack. A mistaken programmer could attempt to free it anyway.

#ifdef BUG_FREE_NOT_MALLOC

/* it is illegal to free a pointer not obtained by malloc (freeing non-heap object) */

free (&head);

#endif

Let’s look at what happens when we compile and run the program with this bug enabled. The commands and results are given below. Even static analysis in current versions of gcc can catch this trivial issue, triggering a warning message. Plus, luckily for system administrators and end users, modern versions of glibc intercept some attempts to free obviously-wrong pointers and immediately terminate the program.

$ gcc -DBUG_FREE_NOT_MALLOC -o llist llist.c

llist.c: In function ‘main’:

llist.c:89:4: warning: attempt to free a non-heap object ‘head’ [-Wfree-nonheap-object]

free (&head);

^

$ ./llist

50, 78, 59, 40, 87, 48, 60, 50, 22, 13,

50, 78, 59, 40, 87, 60, 50, 22, 13,

*** Error in `./llist': double free or corruption (out): 0x00007ffe53fdc130 ***

Aborted (core dumped)

We do not even need to break out Electric Fence to further explore this issue, so let’s move on to another common mistake: freeing a pointer that has already been freed.

Double free

A double free occurs when a program erroneously passes an otherwise-valid pointer to free that has already been freed. The risk of a double free is enhanced by two circumstances. First, C programmers are constantly warned to ensure that allocated memory is freed when no longer needed to avoid memory leaks. Second, there is often confusion over who “owns” a pointer, that is, inconsistency between functions that allocate and return dynamic memory that the caller must free (like strdup or asprintf or OpenSSL’s ASN1_STRING_to_UTF8 which copies a string data structure to an array of char in UTF-8 encoding) versus those that explicitly warn against the programmer erroneously freeing an internal pointer (like OpenSSL’s ASN1_STRING_data which returns a pointer to the raw string data).

Things can get even more confusing and depend on state across multiple function calls. Consider libpng:

If you use png_set_rows(), the application is responsible for freeing row_pointers (and row_pointers[i], if they were separately allocated).

If you don’t allocate row_pointers ahead of time, png_read_png() will do it, and it’ll be free’ed by libpng when you call png_destroy_*().

Bringing C++ into the mix can make things even more complicated. A common error when implementing a C++ class that uses dynamically allocated members is to fail to implement the “big three:” a virtual destructor, a copy constructor, and an overloaded assignment operator. For example, if the programmer implements a virtual destructor that frees all dynamic data members, but fails to implement a copy constructor that duplicates them on a copy (i.e., a deep copy), the compiler will substitute a byte-for-byte copy of the object’s data member pointers (i.e., a shallow copy) instead. When such an object is created, copied to another instance, and then both leave scope, the destructor code will free the same pointers twice.

The llist.c program has a trivial example of a double-free bug when BUG_DOUBLE_FREE is defined, simply calling the list-freeing function (that iterates over and frees each element in the list) twice. Because the lfree function does not reset all next pointers to NULL upon freeing the pointee, this is not safe. A code excerpt is shown below.

lfree (&head);

#ifdef BUG_DOUBLE_FREE

/* it is illegal to call free on the same pointer more than once */

lfree (&head);

#endif

If we compile this code and run it, we see some interesting results. Running the program normally shows no errors; running with the environment variable MALLOC_CHECK_ enabled (which enables more extensive malloc debugging code built in to glibc) returns an invalid pointer error. Running with Electric Fence causes the program to crash with a segmentation fault upon reaching the double free. The commands and output in their respective order are shown below.

$ gcc -DBUG_DOUBLE_FREE -o llist llist.c

$ ./llist

50, 78, 59, 40, 87, 48, 60, 50, 22, 13,

50, 78, 59, 40, 87, 60, 50, 22, 13,

$ MALLOC_CHECK_=1 ./llist

50, 78, 59, 40, 87, 48, 60, 50, 22, 13,

50, 78, 59, 40, 87, 60, 50, 22, 13,

*** Error in `./llist': free(): invalid pointer: 0x0000000001b4b010 ***

Aborted (core dumped)

$ LD_PRELOAD=/usr/lib/libefence.so ./llist



Electric Fence 2.2 Copyright (C) 1987-1999 Bruce Perens

50, 78, 59, 40, 87, 48, 60, 50, 22, 13,

50, 78, 59, 40, 87, 60, 50, 22, 13,

Segmentation fault (core dumped)

We see that glibc, by default, does not detect a classic double free corruption, but when its MALLOC_CHECK_ option is enabled, it does. This variable does come with a performance penalty, so it is disabled by default. Regardless, let’s look at some bugs that glibc fails to detect even when MALLOC_CHECK_ is enabled, requiring us to use Electric Fence or another tool to find them.

Use-after-free

A use-after-free bug (also called a dangling pointer bug) occurs when a pointer is dereferenced (and the pointed-to data therefore read from or written to) after the associated memory is freed (and possibly reallocated). Like other heap management bugs, this was not taken seriously as a security issue until Afek and Sharabani demonstrated an exploitable example in Microsoft Internet Information Services in 2007. Their particular exploit assumes that a C++ object is freed and its memory reallocated for another purpose; if the attacker can fill the reallocated memory with pointers to attacker-controlled memory, subsequent reuse of the C++ object could cause the program to mistreat attacker data as the object’s virtual function table and therefore transfer control to the attacker.

Our program demonstrates a different use-after-free bug, shown below. When destroying a linked list, it is invalid to follow a link from one node to the next if the originating node has already been freed. The programmer must use a temporary variable to hold the pointer to the next node, instead. This bug occurs often because default malloc implementations do not always alter the freed memory in any immediate way, so the intuitive-looking but wrong prev->next = p->next compiles and runs with no issue in nonmalicious environments.

#ifdef BUG_USE_AFTER_FREE

/* it is illegal to dereference a pointer after freeing it */

free (p);

prev->next = p->next;

#else

struct node *temp = p->next;

free (p);

prev->next = temp;

#endif

Of course, in a real attack, the adversary would need to find a way to reallocate the space used by the freed node structure and fill it with malicious data, such as a race condition. Actual exploit development for use-after-free is outside the scope of this document.

This is an instance where Electric Fence is advantageous. Let’s look at the output from running the program with the use-after-free bug, first normally, then using MALLOC_CHECK_, and finally with Electric Fence; all three cases are shown below. Without Electric Fence, the bug goes undetected (even with MALLOC_CHECK_ enabled), but adding Electric Fence causes the program to crash with a segmentation fault.

$ gcc -DBUG_USE_AFTER_FREE -o llist llist.c

$ ./llist

50, 78, 59, 40, 87, 48, 60, 50, 22, 13,

50, 78, 59, 40, 87, 60, 50, 22, 13,

$ MALLOC_CHECK_=1 ./llist

50, 78, 59, 40, 87, 48, 60, 50, 22, 13,

50, 78, 59, 40, 87, 60, 50, 22, 13,

$ LD_PRELOAD=/usr/lib/libefence.so ./llist



Electric Fence 2.2 Copyright (C) 1987-1999 Bruce Perens

50, 78, 59, 40, 87, 48, 60, 50, 22, 13,

Segmentation fault (core dumped)

Note that this bug is an example of a use-after-free when the freed space has not been subsequently reallocated; to ensure that a freed pointer never points to a valid address again in the future and catch more use-after-free bugs, set the environment variable EF_PROTECT_FREE=1. This option does, unfortunately, increase memory usage even further than running Electric Fence alone.

Out-of-bounds write

An out-of-bounds write is often triggered by a classic heap overflow. Because important malloc metadata lie just before and after a block of memory that it returns, overflows invariably corrupt malloc data structures. When launched maliciously, this can cause later malloc or free calls to overwrite memory locations with attacker-controlled data. A sample code example of a trivial heap overflow appears below. By allocating 4 bytes for the array and then copying a string longer than 4 characters using strcpy, the program overflows the memory block for array, and subsequent memory is overwritten.

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

int main ()

{

char *array;

array = (char *)malloc(sizeof(char)*4);

strcpy(array, "more than 4 characters");

return 0;

}

Here is another instance where Electric Fence is useful. As shown below, running this program normally does not return any errors, even if we enable glibc’s MALLOC_CHECK_ environment variable. However, if we enable Electric Fence, the program crashes immediately when the overflow occurs.