Skip to main content

Overview

The malloc playground is an interactive tool designed for learning and experimenting with heap memory allocation behavior. It provides a simple command-line interface to allocate, free, and inspect memory chunks in real-time.
This tool is perfect for understanding malloc behavior before attempting exploitation techniques.

Building and Running

1

Compile the program

gcc malloc_playground.c -o malloc_playground
2

Run the playground

./malloc_playground
The program will display its PID (process ID) for attaching debuggers like GDB.
3

Start experimenting

Use the available commands to allocate and free memory, then observe the behavior.

Available Commands

The playground supports multiple commands for interacting with heap memory:

Basic Commands

# Allocate n bytes and store the pointer
malloc 64
==> OK, 0x5555557592a0

List and Inspection Commands

# List all active pointers with their data
0 - 0x5555557592a0 - hello - 64
1 - 0x555555759300 - world - 32
==> ok

glibc-Specific Commands

These commands are only available when compiled with glibc on Linux systems.
# Display malloc statistics
Arena 0:
system bytes     = 135168
in use bytes     = 1024
Total (incl. mmap):
system bytes     = 135168
in use bytes     = 1024
max mmap regions = 0
max mmap bytes   = 0

Source Code

Here’s the complete source code for the malloc playground:
Note: The write command has a potential buffer overflow vulnerability (no bounds checking). This is intentional for educational purposes.
malloc_playground.c
#include <inttypes.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#ifdef __GLIBC__
# include <malloc.h>
# include <mcheck.h>
void print_mcheck_status(enum mcheck_status s)
{
	fprintf(stderr, "%s\n", (s == MCHECK_DISABLED) ? "N/A, you didn't enable mcheck()" :
			   (s == MCHECK_OK) ? "No inconsistency detected" :
			   (s == MCHECK_HEAD) ? "Memory preceding an allocated block was clobbered" :
			   (s == MCHECK_TAIL) ? "Memory following an allocated block was clobbered" :
			   (s == MCHECK_FREE) ? "A block of memory was freed twice" :
			   "unknown memory check code!");
}
void report_mcheck_fail(enum mcheck_status s)
{
	fprintf(stderr, "*** PROGRAM WOULD ABORT: "); print_mcheck_status(s);
}
#endif

#define MAX_PTR_NUM 20;

char **ptrArray;

int main(int argc, char ** argv) {
	int num;
	int ptrNumber = -1;
	int maxPtr = MAX_PTR_NUM;
	int sizeArg;
	char sizeTable[maxPtr];

	char buffer[1000];
	char cmd[1000];
	char arg1[100];
	char arg2[100];

	memset(sizeTable, 0, maxPtr);
	memset(cmd, 0, 1000);
	memset(arg1, 0, 100);
	memset(arg2, 0, 100);

	fprintf(stderr, "pid: %d\n", getpid());
	ptrArray = malloc(sizeof(char*) * 20);
	for (int i = 0; i < maxPtr; i++){
		ptrArray[i] = 0;
	}
	while (1) {
		fprintf(stderr, "> ");
		fgets(buffer, sizeof(buffer), stdin);
		num = sscanf(buffer, "%s %s %s\n", cmd, arg1, arg2);
		if (strcmp(cmd, "malloc") == 0) {
			if (ptrNumber < maxPtr){
				sizeArg = atoi((const char *) &arg1);
				void *result = malloc(sizeArg);
				ptrNumber++;
				sizeTable[ptrNumber] = sizeArg;
				ptrArray[ptrNumber] = result;
				strcpy(result, "none");
				fprintf(stderr, "==> OK, %p\n", result);
			}
			else{
				printf("Max pointer reached, free or restart");
			}
		} else if (strcmp(cmd, "free") == 0) {
			if (ptrNumber > -1){
				if (num == 1){
					free((void*) ptrArray[ptrNumber]);
					ptrArray[ptrNumber] = 0;
					sizeTable[ptrNumber] = 0;
					fprintf(stderr, "==> ok\n");
					ptrNumber -= 1;
				}
				else if (num == 2){
					int tmpArg = atoi((const char *) &arg1);
					ptrArray[tmpArg] = 0;
					sizeTable[tmpArg] = 0;
					free((void *) ptrArray[tmpArg]);
					ptrNumber -= 1;
					fprintf(stderr, "==> ok\n");
				}
			}
			else{
				fprintf(stderr, "==> list empty :/\n");

			}
		} else if (strcmp(cmd, "write") == 0) {
			if (num == 1){
				printf("write: write value [pointer index]\n");
			}
			else if (num == 2){
				int len = strlen((const char *) &arg1);
				strcpy(ptrArray[ptrNumber], (const char *) &arg1);
				fprintf(stderr, "==> ok, wrote %s\n", ptrArray[ptrNumber]);
			}
			else if (num == 3){
				int len = strlen((const char *) &arg1);
				int tmpArg2 = atoi((const char *) &arg2);
				strcpy(ptrArray[tmpArg2], (const char *) &arg1);
				fprintf(stderr, "==> ok, wrote %s\n", ptrArray[tmpArg2]);
			}
		} else if (strcmp(cmd, "listp") == 0) {
			printf("\n");
			for (int i = 0; i < 20; i++){
				if (ptrArray[i]){
					printf("%d - %p - %s - %d\n", i, ptrArray[i], ptrArray[i], sizeTable[i]);
				}
			}
			fprintf(stderr, "==> ok\n");
		} else if (strcmp(cmd, "listpall") == 0) {
			int tmpIndex = 0;
			printf("\n");
			for (int i=0; i < maxPtr; i++){
				printf("%d - %p - %s - %d\n", tmpIndex, ptrArray[tmpIndex], ptrArray[tmpIndex], sizeTable[i]);
				tmpIndex++;
			}
			fprintf(stderr, "==> ok\n");
		} else if (strcmp(cmd, "clearlist") == 0) {
			ptrNumber = -1;
			for (int i = 0; i < maxPtr; i++){
				free(ptrArray[i]);
				ptrArray[i] = 0;
				memset(sizeTable, 0, maxPtr);
		}
			fprintf(stderr, "==> ok, array cleared\n");
#ifdef __GLIBC__
		} else if (strcmp(cmd, "usable") == 0) {
			fprintf(stderr, "usable size: %zu\n", malloc_usable_size((void*) arg1));
		} else if (strcmp(cmd, "stats") == 0) {
			malloc_stats();
		} else if (strcmp(cmd, "info") == 0) {
			malloc_info(0, stdout);
			printf("Ptrptr %d\n", ptrNumber);
		} else if (strcmp(cmd, "mcheck") == 0) {
			fprintf(stderr, "==> %s\n", mcheck(report_mcheck_fail) == 0 ? "OK" : "ERROR");
		} else if (strcmp(cmd, "mcheck_pedantic") == 0) {
			fprintf(stderr, "==> %s\n", mcheck_pedantic(report_mcheck_fail) == 0 ? "OK" : "ERROR");
		} else if (strcmp(cmd, "mprobe") == 0) {
			if (num > 1) {
				print_mcheck_status(mprobe((void*) arg1));
			} else {
				mcheck_check_all();
				fprintf(stderr, "==> check_all ok\n");
			}
#endif
		} else {
			puts("Commands: malloc n, free p, usable p, stats, info, mprobe [p], mcheck, mcheck_pedantic, ");
			puts("Commands: [BETA]  write str, listp, listpall, clearlist\n");
		}
	}
}

Example Session

Here’s a typical session demonstrating basic heap behavior:
Session 1: Basic allocation and reuse
pid: 12345
> malloc 64
==> OK, 0x555555759260
> malloc 32
==> OK, 0x5555557592a0
> malloc 64
==> OK, 0x5555557592e0
> listp

0 - 0x555555759260 - none - 64
1 - 0x5555557592a0 - none - 32
2 - 0x5555557592e0 - none - 64
==> ok
> free 1
==> ok
> malloc 32
==> OK, 0x5555557592a0  # Same address reused!
Notice how the freed chunk at index 1 is immediately reused when we allocate the same size again. This demonstrates first-fit allocation.

Use Cases

Learning

Understand how malloc reuses freed chunks and organizes memory

Experimentation

Test allocation patterns before implementing exploits

Debugging

Use with GDB to inspect heap state visually

Teaching

Demonstrate heap concepts in a controlled environment

Tips for Using the Playground

Attach a debugger: The program prints its PID so you can attach GDB:
gdb -p <PID>
Then use GDB commands like heap chunks (with gef/pwndbg) to visualize the heap.
Use mcheck early: Enable mcheck before making allocations to catch corruption:
> mcheck
==> OK
> malloc 64
...
Experiment with tcache: Allocate and free 7 chunks of the same size to fill a tcache bin, then observe what happens with the 8th chunk.

Next Steps

First Fit

See first-fit allocation in action

Heap Basics

Review fundamental heap concepts

Build docs developers (and LLMs) love