PHP Extension Heap Overflow Explained: When Metadata Owns the Allocator
You upload a picture. The server extracts the title and artist for you. The gallery looks clean. What you do not see is a small native extension that allocates a fixed 56-byte buffer and then calls strcpy as if length checks were optional. That is the moment the picture stops being decoration and starts rewriting process memory.
This article covers the idea from first principles, a complete synthetic scenario, the vulnerable patterns, and the practical fixes.
What Is a PHP Extension Heap Overflow, Anyway?
PHP extensions are native C code loaded into the PHP process. They talk to the Zend Memory Manager, not to the system malloc. Zend splits memory into fixed-size bins. When an extension asks for 56 bytes it receives a block from the matching bin. That block lives on a free list that is essentially a singly-linked list of forward pointers.
If the extension then performs an unbounded strcpy into the block, the overflow walks into the next chunk or straight into the free-list metadata. Because a free-list entry is just a pointer, overwriting it lets you decide where the next allocation will land. The path from "pretty image metadata" to "I control the allocator" is short.
The second common ingredient is an information leak. Without base addresses the write is blind. A Local File Inclusion (or any primitive that can read /proc/self/maps) supplies the missing addresses. Once the attacker can aim the corrupted free list at a GOT entry, swapping a function pointer for system turns the next free into a command.
Why Should You Care?
- Custom extensions rarely receive the same audit attention as core PHP.
- Image-processing and metadata libraries are frequent attack surface because users are expected to upload files.
- Zend small-bin free lists perform almost no integrity checks, so a single overflow is often enough for arbitrary allocation.
-
Once you can free an attacker-controlled string through a hooked
_efree, you obtain RCE without needing extra gadgets. - The same pattern appears in any language runtime that uses size-class allocators and trusts native code.
Worked Example: Gallery That Trusts Its Metadata
Imagine a photo gallery. Users upload PNG files. A custom extension extracts three text fields: Title, Artist, and Copyright. For each field the extension allocates a fixed-size buffer and copies the text with strcpy. The gallery also exposes a view endpoint that accepts a path parameter and returns the file contents as a base64 data URI. The endpoint performs only a shallow existence check.
Discovery
The view endpoint accepts absolute paths. Reading /proc/self/maps reveals the load addresses of the extension, libc, and the main PHP binary. That single request defeats ASLR for the rest of the attack.
Uploading a PNG whose Title field exceeds 55 bytes produces garbled output or a crash, confirming the overflow. Multiple text chunks that share the same key cause repeated allocations from the same bin and give control over the shape of the free list.
Exploitation Path
- Leak the maps and record the base of the extension and of libc.
- Craft a PNG whose text chunks allocate three consecutive 56-byte blocks.
-
Overflow the first block so the free-list pointer of the second block now points at a chosen address (for example a GOT entry for
_efreeinside the extension). - On the next allocation the allocator hands out the attacker-chosen address.
-
Write the address of
systeminto that location. -
Force a free of a string that contains the desired command. The hooked
_efreebecomessystem(command).
A practical command can append a directory listing to a file that the LFI can later read, or open a reverse shell. Once the process is under control the rest is ordinary post-exploitation.
Adjacent Variants
- The same overflow works against any size-class allocator that stores free-list metadata inside the chunk.
-
If the extension uses
strncpywith a length taken from attacker-controlled text, the bug simply moves to an integer overflow or a missing null terminator. - When the view endpoint is hardened but an error page or log file still discloses absolute paths, the information leak can still be obtained.
Vulnerable Code Examples
C side (the extension)
// Vulnerable
char *buf = emalloc(56);
strcpy(buf, text_chunk->text); // unbounded copy
meta->title = buf;
// Patched
size_t len = strlen(text_chunk->text);
if (len >= 56) {
php_error_docref(NULL, E_WARNING, "metadata too long");
return;
}
char *buf = emalloc(len + 1);
memcpy(buf, text_chunk->text, len + 1);
meta->title = buf;
PHP side (the file viewer)
// Vulnerable
$path = urldecode($_GET['file']);
if (file_exists($path)) {
echo base64_encode(file_get_contents($path));
}
// Patched
$path = realpath($_GET['file'] ?? '');
$base = realpath(__DIR__ . '/uploads');
if ($path === false || strpos($path, $base) !== 0) {
http_response_code(403);
exit;
}
echo base64_encode(file_get_contents($path));
Defense / How to Fix
-
Never use unbounded string-copy functions in extensions. Prefer
memcpywith an explicit length check, or the Zend string APIs that carry length. - Treat every text-chunk length as attacker-controlled. Cap it hard and reject oversized fields early.
- Compile extensions with stack and heap canaries, and consider AddressSanitizer during development.
-
Restrict any file-serving endpoint to a known directory using
realpathand a prefix check. -
Disable or carefully sandbox the ability to read
/procfrom the web process if it is not required. - Keep the extension's GOT and other writable sections as small as possible; use full RELRO when linking.
- Audit every custom extension with the same threat model you apply to the main application.
Final Thoughts
A picture should not be able to rewrite the process that displays it. When a native extension mixes fixed-size allocations with classic C string functions, the allocator becomes just another writable surface. Pair that with an information leak and the rest of the attack is almost mechanical.
The safest metadata parser is the one that never trusts the length of the data it is given.