In August 2016, Check Point researcher Yannay Livneh disclosed three zero-days in PHP 7's unserialize() implementation at once — CVE-2016-7478, CVE-2016-7479, and CVE-2016-7480 — each capable of remote code execution against nothing more than a crafted string an application passed to unserialize(). CVE-2016-7479 alone let an attacker resize an object's internal properties hash table mid-deserialization and have the Zend engine dereference freed memory afterward. That wasn't PHP's first unserialize use-after-free vulnerability (UAF), and it wasn't its last: researcher Taoguang Chen's 2015 disclosures — including HackerOne report #159948 covering SplDoublyLinkedList — had already documented the same UAF pattern spreading across SplArrayObject (CVE-2015-6831) and DateInterval (CVE-2015-0273), and PHP's own bug tracker logged a related garbage-collector double-free (bug #72433, CVE-2016-5771) the same year as the Check Point trio. Nearly a decade later, PHP still has no memory-safe rewrite — only incremental hardening, including the unserialize() allowed-classes option PHP 7.0 shipped specifically in response to this bug class. What these disclosures reveal generalizes well past PHP: reference-counted, mutable runtimes create UAF risk even when developers never touch raw pointers. This piece walks through what actually happened, why, and what it means for teams shipping on any dynamic-language runtime.
What made PHP's unserialize() such a persistent source of use-after-free bugs?
The root cause is structural, not a one-off coding mistake: unserialize() reconstructs arbitrary object graphs from attacker-controlled bytes, and PHP lets those objects run code — via __wakeup(), __destruct(), and SPL magic methods — in the middle of that reconstruction. A destructor invoked mid-deserialization can free or resize a data structure the engine's C code still holds a raw pointer to, and the next access after that free is the use-after-free. Livneh's 2016 Check Point research (later presented as "Exploiting PHP7 unserialize") showed this concretely: triggering garbage collection while an object's properties table was being resized left a dangling reference the engine dereferenced moments later, a bug that wasn't closed until the fix landed in PHP 7.0.15. The earlier 2015 disclosures against SplDoublyLinkedList (HackerOne #159948) and SplArrayObject (CVE-2015-6831) followed the identical pattern one PHP version series earlier. The bug class kept recurring because the fix each time patched one interaction, not the underlying hazard of running arbitrary code mid-parse.
Were these bugs isolated incidents or a recurring pattern?
They were unmistakably a pattern, spanning multiple PHP branches over roughly two years. CVE-2016-7478 (php_zip.c/ZipArchive) and the SPL-array UAF affected PHP before 5.5.37, 5.6.x before 5.6.23, and 7.x before 7.0.8 — three concurrently maintained branches, all vulnerable to the same class of garbage-collector interaction bug, tracked internally as PHP bug #72433 (CVE-2016-5771) and bug #72434 (CVE-2016-7478). CVE-2016-7480 hit ext/spl/spl_observer.c's SplObjectStorage unserialization, which failed to verify a deserialized key was actually an object before dereferencing it as one — a type-confusion bug adjacent to, but distinct from, a classic UAF. And Taoguang Chen's broader 2015 research line had already found the same family of bug in DateInterval (CVE-2015-0273) a year earlier. Each fix closed one code path; none addressed why PHP's object graph reconstruction and its reference-counting garbage collector kept producing dangling pointers when combined.
Is CVE-2019-11043 part of the same bug family?
No — and conflating it with the unserialize UAFs is a common but incorrect shorthand worth correcting. CVE-2019-11043 is a stack/integer underflow in PHP-FPM's fpm_main.c, triggered by a malformed PATH_INFO value on Nginx configurations that pass PATH_INFO through to PHP-FPM; it was first reported to PHP's bug tracker by researcher Emil Lerner in September 2019 after Andrew Danau of Wallarm spotted the anomaly during a CTF, and Qualys published the RCE analysis that made it widely known. It's memory corruption, and it is a real, serious PHP RCE — patched in 7.1.33, 7.2.24, and 7.3.11 — but it's an out-of-bounds write in request parsing, not a reference-counting or destructor-triggered UAF. Lumping every "PHP memory bug" into one bucket obscures that these are two structurally different failure modes: one lives in how PHP parses raw CGI input, the other in how its object model handles cleanup during deserialization.
Is this risk unique to PHP, or does it show up in other dynamic runtimes?
It is not unique to PHP — it's intrinsic to any runtime that mixes reference counting with user-overridable cleanup hooks. The same shape of bug — a destructor or finalizer running mid-operation, freeing memory a lower-level routine still expects to be valid — has surfaced in CPython's C extension API around reference-count management, in Ruby's object finalization, and in V8's garbage collector when JavaScript callbacks run during a collection pass. The pattern is: the interpreter is written in C or C++, the interpreter exposes hooks that let script-level code run at points the interpreter author didn't fully anticipate, and one of those hook executions mutates state the surrounding native code is still using. Language-level memory safety in the scripting layer (no pointer arithmetic, no manual free()) doesn't eliminate this — the vulnerability lives one layer down, in the runtime's own C implementation, which is exactly why "written in a memory-safe language" and "immune to use-after-free" are not the same claim for any interpreted language whose runtime itself is native code.
What should teams running PHP (or any dynamic runtime) actually do about this?
Treat unserialize() of any untrusted input as a standing use-after-free vulnerability waiting to be found, not a one-time patch target. PHP 7.0's unserialize() allowed-classes option (an explicit second argument restricting which classes may be instantiated) exists precisely so applications can neutralize the entire object-graph-reconstruction attack surface without waiting for the next point release; using json_decode() for untrusted data removes it entirely. Beyond that, staying current on PHP's supported branches matters more here than in most ecosystems, because these fixes are backport-only — there is no forthcoming memory-safe PHP runtime rewrite to age out the risk. For teams managing PHP dependencies at scale, Composer manifests and lockfiles are worth treating as first-class software composition analysis (SCA) inputs: Safeguard resolves composer.lock as part of its dependency scanning, so a pinned PHP package still running an unserialize-vulnerable version shows up as a tracked, versioned finding rather than an assumption nobody checks twice.