October 22, 2026
PHP2Shell Explained: Understanding CVE-2026–109618 and CVE-2026–109619 from the Memory Up
Two critical PHP vulnerabilities, CVE-2026-109618 and CVE-2026-109619, provide an excellent opportunity to understand how seemingly small…

By Yanis BOUDJELIDA
13 min read
- 1 Two critical PHP vulnerabilities, CVE-2026-109618 and CVE-2026-109619, provide an excellent opportunity to understand how seemingly small mistakes in memory management can eventually become Remote Code Execution vulnerabilities.
- 2 1. The concepts you need to understand
- 3 2. Processes and memory
- 4 3. Stack vs Heap
- 5 Stack
Two critical PHP vulnerabilities, CVE-2026-109618 and CVE-2026-109619, provide an excellent opportunity to understand how seemingly small mistakes in memory management can eventually become Remote Code Execution vulnerabilities.
Understanding them properly requires more than memorizing that one involves an Out-of-Memory condition and the other a Use-After-Free. We first need to understand how memory, pointers, PHP workers, request lifecycles, and memory allocators interact.
This article builds those concepts from the ground up before connecting them to the vulnerabilities.
1. The concepts you need to understand
Before analyzing the CVEs, we need the following concepts:
- Process and process memory
- Stack and heap
- Memory allocation
- Pointers and memory addresses
malloc()andfree()- Logical memory release vs physical erasure
- Memory allocator
- Dangling pointers
- Use-After-Free
- Memory reuse
- Heap grooming
- Memory corruption
- Out-of-Memory conditions
memory_limit- Partial initialization
- Program invariants
- Error handling and
longjmp() - Global state
- Request-scoped memory
- PHP workers
- Worker persistence
- Request lifecycle
mod_php- PHP-FPM
- Producer/consumer exploitation patterns
- Memory exploitation primitives
- Remote Code Execution
Let's examine them one by one.
2. Processes and memory
When PHP executes, it runs inside a process.
A simplified representation is:
PHP process
┌──────────────────────┐
│ Program code │
├──────────────────────┤
│ Global data │
├──────────────────────┤
│ Heap │
│ │
│ Dynamic allocations │
│ │
├──────────────────────┤
│ Stack │
│ Local variables │
│ Function calls │
└──────────────────────┘PHP process
┌──────────────────────┐
│ Program code │
├──────────────────────┤
│ Global data │
├──────────────────────┤
│ Heap │
│ │
│ Dynamic allocations │
│ │
├──────────────────────┤
│ Stack │
│ Local variables │
│ Function calls │
└──────────────────────┘The operating system gives this process its own virtual address space.
Addresses could conceptually look like:
0x1000
0x2000
0x3000
...0x1000
0x2000
0x3000
...These addresses allow the program to locate its data.
3. Stack vs Heap
Two important memory areas are the stack and the heap.
Stack
The stack is mainly associated with function execution and local variables.
For example:
void hello() {
int age = 22;
}void hello() {
int age = 22;
}age can live on the stack.
When hello() returns, its stack frame disappears automatically.
Conceptually:
hello()
STACK
┌──────────┐
│ age = 22 │
└──────────┘
return
STACK
┌──────────┐
│ │
└──────────┘hello()
STACK
┌──────────┐
│ age = 22 │
└──────────┘
return
STACK
┌──────────┐
│ │
└──────────┘Heap
The heap is used for dynamically allocated objects whose lifetime cannot simply be tied to a function returning.
Conceptually:
ptr = malloc(100);ptr = malloc(100);could produce:
HEAP
0x5000
┌───────────────────┐
│ 100-byte region │
└───────────────────┘
ptr = 0x5000HEAP
0x5000
┌───────────────────┐
│ 100-byte region │
└───────────────────┘
ptr = 0x5000PHP makes extensive use of dynamic memory for strings, arrays, hash tables, objects, request data, parser structures and other runtime information.
Both CVEs discussed in this article are therefore closely related to heap and object lifetime management.
4. Pointers
A pointer is fundamentally a variable containing a memory address.
Suppose:
ptr = 0x5000ptr = 0x5000Then:
ptr
│
▼
0x5000
┌─────────────┐
│ Object A │
└─────────────┘ptr
│
▼
0x5000
┌─────────────┐
│ Object A │
└─────────────┘The important distinction is:
pointer ≠ objectpointer ≠ objectThe pointer contains the location of the object.
This becomes critical when the lifetime of the object and the lifetime of the pointer become inconsistent.
5. Memory allocation
When PHP needs memory, it asks its memory management system for an allocation.
Simplifying heavily:
ptr = malloc(size);ptr = malloc(size);The allocator finds an available region:
Heap
0x1000 [used]
0x2000 [FREE]
0x3000 [used]Heap
0x1000 [used]
0x2000 [FREE]
0x3000 [used]and may return:
ptr = 0x2000ptr = 0x2000Now:
0x2000 [used]0x2000 [used]The exact mechanisms used by PHP and the underlying allocator are more complicated, but this model is sufficient for understanding the vulnerabilities.
6. What free() really means
Suppose:
ptr = 0x2000
0x2000
┌─────────────┐
│ Object A │
└─────────────┘ptr = 0x2000
0x2000
┌─────────────┐
│ Object A │
└─────────────┘Then the program performs:
free(ptr);free(ptr);A common misconception is that the memory becomes physically empty.
It does not necessarily.
free() primarily means:
This allocation is no longer owned by the caller and may be reused.
Conceptually:
BEFORE
0x2000 [Object A]
AFTER free()
0x2000 [AVAILABLE]BEFORE
0x2000 [Object A]
AFTER free()
0x2000 [AVAILABLE]Some of the old bytes might temporarily remain, while the allocator may also overwrite parts of the region with its own metadata.
The important property is that accessing the allocation through the old pointer is no longer valid.
7. Why the pointer still contains the address
This surprises many people.
Consider:
ptr = malloc(...);
free(ptr);ptr = malloc(...);
free(ptr);free(ptr) does not generally perform:
ptr = NULL;ptr = NULL;Therefore:
Before:
ptr = 0x2000
After:
ptr = 0x2000Before:
ptr = 0x2000
After:
ptr = 0x2000The allocator knows that the allocation associated with 0x2000 has been released.
But ptr is just a variable containing the number 0x2000.
It does not automatically know:
"is_free = true""is_free = true"This distinction is fundamental.
8. The memory allocator
Think of the allocator as a hotel receptionist.
It keeps track of rooms:
Room 20 → occupied
Room 21 → occupied
Room 22 → free
Room 23 → occupiedRoom 20 → occupied
Room 21 → occupied
Room 22 → free
Room 23 → occupiedIf somebody requests a suitable room, the receptionist can assign Room 22.
Similarly:
malloc(...)
↓
allocator searches for suitable memory
↓
returns addressmalloc(...)
↓
allocator searches for suitable memory
↓
returns addressAfter:
free(ptr);free(ptr);the allocator can mark that allocation as available for reuse.
This does not invalidate every other variable in the program that happens to contain the same address.
9. Dangling pointers
Consider:
ptr = 0x2000ptr = 0x2000followed by:
free(ptr);free(ptr);We now have:
ptr
│
▼
0x2000 [FREE]ptr
│
▼
0x2000 [FREE]ptr is called a dangling pointer.
It references memory whose original allocation is no longer alive.
A common defensive pattern is:
free(ptr);
ptr = NULL;free(ptr);
ptr = NULL;But even that does not solve every lifetime problem if aliases exist:
ptr1 = 0x2000;
ptr2 = 0x2000;
free(ptr1);
ptr1 = NULL;ptr1 = 0x2000;
ptr2 = 0x2000;
free(ptr1);
ptr1 = NULL;Now:
ptr1 = NULL
ptr2 = 0x2000
↓
freed allocationptr1 = NULL
ptr2 = 0x2000
↓
freed allocationptr2 is still dangling.
10. Use-After-Free
A dangling pointer becomes especially dangerous when the program uses it.
Hence:
Use After Free.
Sequence:
allocate
↓
ptr → Object A
↓
free(Object A)
↓
ptr still contains address
↓
use ptr
↓
USE-AFTER-FREEallocate
↓
ptr → Object A
↓
free(Object A)
↓
ptr still contains address
↓
use ptr
↓
USE-AFTER-FREESometimes this simply crashes the process.
Sometimes it does something much more dangerous.
11. Memory reuse
Suppose:
ptr = 0x2000ptr = 0x2000and then:
free(ptr);free(ptr);The allocator now knows:
0x2000 = available0x2000 = availableLater, another allocation occurs.
The allocator may reuse the same region:
new_ptr = 0x2000new_ptr = 0x2000Now:
old ptr ──────┐
▼
0x2000
┌───────────┐
│ Object B │
└───────────┘
▲
│
new_ptr ───────┘old ptr ──────┐
▼
0x2000
┌───────────┐
│ Object B │
└───────────┘
▲
│
new_ptr ───────┘The old code may conceptually believe:
ptr → Object Aptr → Object Awhile reality is:
0x2000 → Object B0x2000 → Object BIf the program interprets Object B according to Object A's structure, memory corruption can follow.
12. Heap grooming
This leads to an important exploitation concept: heap grooming.
An attacker may try to influence the arrangement of heap allocations.
Imagine:
AAAA | BBBB | CCCC | DDDDAAAA | BBBB | CCCC | DDDDThen BBBB is released:
AAAA | FREE | CCCC | DDDDAAAA | FREE | CCCC | DDDDA stale pointer still references that location.
If a suitable later allocation occupies it:
AAAA | XXXX | CCCC | DDDDAAAA | XXXX | CCCC | DDDDthe stale pointer now references XXXX.
Conceptually:
stale pointer
│
▼
AAAA | XXXX | CCCC | DDDDstale pointer
│
▼
AAAA | XXXX | CCCC | DDDDThe goal of heap grooming is to make memory layout and reuse sufficiently predictable for exploitation.
It is important to understand that a Use-After-Free does not automatically mean RCE. Turning memory unsafety into reliable execution control is usually the difficult part.
13. Memory corruption
Memory corruption occurs when a program's assumptions about its memory are violated.
Examples include:
Buffer Overflow
Use-After-Free
Double Free
Out-of-bounds read/write
Uninitialized memory
Invalid pointer useBuffer Overflow
Use-After-Free
Double Free
Out-of-bounds read/write
Uninitialized memory
Invalid pointer useThe two PHP vulnerabilities discussed here involve different paths toward unsafe memory state.
14. Out Of Memory — OOM
OOM means:
Out Of Memory.
PHP commonly enforces a memory limit such as:
memory_limit = 128Mmemory_limit = 128MSuppose PHP has already accounted for approximately:
127 MB127 MBand needs another allocation.
Conceptually:
127 MB used
+
2 MB requested
──────────────
129 MB
limit = 128 MB127 MB used
+
2 MB requested
──────────────
129 MB
limit = 128 MBThe allocation cannot proceed under that limit.
An OOM condition occurs.
OOM itself is not necessarily a security vulnerability.
The vulnerability depends on what state the program is in when the allocation fails and how that failure is handled.
15. Partial initialization
Imagine PHP needs to create:
Structure X
A
B
C
DStructure X
A
B
C
DInitialization progresses:
A = initialized
B = initialized
C = initializing
D = not initializedA = initialized
B = initialized
C = initializing
D = not initializedThen an allocation fails.
The structure is now only partially initialized.
A robust design must ensure that other code cannot mistakenly treat:
[A][B][?][?][A][B][?][?]as:
[A][B][C][D][A][B][C][D]This concept is central to CVE-2026-109618.
16. Program invariants
An invariant is an assumption that programmers expect to always remain true.
For example:
If global_context != NULL,
then global_context points to a fully initialized object.If global_context != NULL,
then global_context points to a fully initialized object.Code elsewhere can then safely do:
if (global_context != NULL)
cleanup(global_context);if (global_context != NULL)
cleanup(global_context);without rechecking every field.
If an unusual error path leaves:
global_context != NULLglobal_context != NULLbut the underlying structure is incomplete, the invariant has been broken.
The cleanup code is not necessarily wrong by itself.
The problem happened earlier when the system entered a state that was supposed to be impossible.
17. longjmp()
C provides mechanisms such as setjmp() and longjmp() for non-local control flow.
For our conceptual model, think of longjmp() as:
Stop what you are doing and jump back to a previously established recovery point.
Instead of:
Function C
↓
return error
↓
Function B handles error
↓
return error
↓
Function A handles errorFunction C
↓
return error
↓
Function B handles error
↓
return error
↓
Function A handles erroryou can conceptually get:
Function A
↓
Function B
↓
Function C
↓
ERROR
│
│ longjmp
│
└──────────────► recovery pointFunction A
↓
Function B
↓
Function C
↓
ERROR
│
│ longjmp
│
└──────────────► recovery pointThis is not inherently insecure.
The danger appears when code between those locations temporarily violates invariants and expects normal control flow to restore them.
18. Request-scoped memory
PHP handles HTTP requests.
A request may cause PHP to allocate memory for:
POST data
uploaded files
strings
arrays
objects
session information
parser metadata
errors
...POST data
uploaded files
strings
arrays
objects
session information
parser metadata
errors
...Much of that state only needs to exist while the current request is alive.
Conceptually:
Request A begins
↓
allocate request memory
↓
process request
↓
Request A ends
↓
release request memoryRequest A begins
↓
allocate request memory
↓
process request
↓
Request A ends
↓
release request memoryThis distinction becomes essential for CVE-2026-109619.
19. Global state
Some state belongs to the longer-lived PHP runtime or worker rather than exclusively to one request.
Conceptually:
WORKER
│
├── global state
│
├── Request A memory
│
└── other runtime stateWORKER
│
├── global state
│
├── Request A memory
│
└── other runtime stateA dangerous situation appears when:
global state
│
▼
memory belonging to Request Aglobal state
│
▼
memory belonging to Request ARequest A eventually disappears.
If the global reference survives, we can get:
global state
│
▼
FREED MEMORYglobal state
│
▼
FREED MEMORYThat is precisely the kind of lifetime mismatch relevant to CVE-2026-109619.
20. PHP workers
A worker is a process that handles requests.
For example:
PHP-FPM master
│
├── Worker 1
├── Worker 2
├── Worker 3
└── Worker 4PHP-FPM master
│
├── Worker 1
├── Worker 2
├── Worker 3
└── Worker 4Workers process PHP requests.
Crucially, workers are commonly reused.
Worker 2
Request A
↓
finish
↓
Request B
↓
finish
↓
Request CWorker 2
Request A
↓
finish
↓
Request B
↓
finish
↓
Request CThis improves performance because PHP does not need to create a completely new process for every request.
But it also means that bugs involving improperly reset worker state can cross request boundaries.
21. Request lifecycle
A simplified PHP request lifecycle looks like:
HTTP request
↓
request startup
↓
input parsing
↓
PHP initialization
↓
application execution
↓
request shutdown
↓
cleanup
↓
worker reusedHTTP request
↓
request startup
↓
input parsing
↓
PHP initialization
↓
application execution
↓
request shutdown
↓
cleanup
↓
worker reusedBoth CVEs become much easier to understand once we ask:
At which stage does the state become inconsistent?
22. mod_php
With mod_php, PHP is loaded directly into Apache.
Conceptually:
Apache process
┌───────────────────────┐
│ Apache │
│ │
│ PHP module │
│ │
└───────────────────────┘Apache process
┌───────────────────────┐
│ Apache │
│ │
│ PHP module │
│ │
└───────────────────────┘A request can therefore be handled inside an Apache process containing the PHP runtime.
This integration matters for the demonstrated CVE-2026-109618 exploitation path because of how PHP startup failure and shutdown interact in this environment.
23. PHP-FPM
PHP-FPM separates the web server from PHP execution.
For example:
Internet
↓
Nginx
↓
FastCGI
↓
PHP-FPM master
│
├── PHP worker
├── PHP worker
└── PHP workerInternet
↓
Nginx
↓
FastCGI
↓
PHP-FPM master
│
├── PHP worker
├── PHP worker
└── PHP workerApache can also use PHP-FPM.
The lifecycle and failure behavior therefore differ from mod_php, which can affect exploitability.
24. Producer and Consumer
This model is especially useful for CVE-2026-109619.
The producer creates dangerous stale state.
Request A
↓
creates state
↓
state references Request A memory
↓
abnormal path
↓
Request A memory released
↓
reference survivesRequest A
↓
creates state
↓
state references Request A memory
↓
abnormal path
↓
Request A memory released
↓
reference survivesThe consumer later uses it:
Request B
↓
same worker
↓
reads stale state
↓
Use-After-FreeRequest B
↓
same worker
↓
reads stale state
↓
Use-After-FreeSo:
PRODUCER CONSUMER
Request A Request B
│ │
▼ ▼
create state same worker
│ │
▼ ▼
free memory ───────────► stale state
│
▼
UAFPRODUCER CONSUMER
Request A Request B
│ │
▼ ▼
create state same worker
│ │
▼ ▼
free memory ───────────► stale state
│
▼
UAF25. Exploitation primitives
A memory corruption bug is not synonymous with RCE.
Attackers generally need useful capabilities, often called primitives.
Examples include the ability to:
read memory
write memory
control a pointer
control an object field
influence allocation placementread memory
write memory
control a pointer
control an object field
influence allocation placementA simplified exploitation progression is:
memory bug
↓
useful memory primitive
↓
stronger control
↓
control-flow manipulation
↓
RCEmemory bug
↓
useful memory primitive
↓
stronger control
↓
control-flow manipulation
↓
RCEThe exact exploitation chain depends heavily on the runtime, allocator, build, architecture and mitigations.
26. Remote Code Execution
RCE means:
Remote Code Execution.
An attacker can cause the remote system to execute attacker-chosen code.
Conceptually:
Attacker
│
│ network
▼
Web server
│
▼
vulnerability
│
▼
memory corruption
│
▼
execution control
│
▼
RCEAttacker
│
│ network
▼
Web server
│
▼
vulnerability
│
▼
memory corruption
│
▼
execution control
│
▼
RCERCE does not automatically mean:
rootrootThe resulting code normally starts with the privileges of the compromised process.
For a web service that may be an account such as:
www-datawww-dataFurther privilege escalation would be a separate step.
Part II — Connecting everything to CVE-2026-109618
Now we can understand the first vulnerability properly.
The important concepts are:
memory_limit
+
OOM
+
partial initialization
+
broken invariant
+
non-local error handling
+
request shutdown
=
memory corruptionmemory_limit
+
OOM
+
partial initialization
+
broken invariant
+
non-local error handling
+
request shutdown
=
memory corruptionA simplified sequence is:
HTTP request
↓
PHP parses input
↓
allocations occur
↓
PHP approaches memory_limit
↓
internal structure starts changing
↓
another allocation is required
↓
OOM
↓
error handling interrupts operation
↓
intermediate state survives
↓
request shutdown
↓
cleanup assumes valid state
↓
memory corruptionHTTP request
↓
PHP parses input
↓
allocations occur
↓
PHP approaches memory_limit
↓
internal structure starts changing
↓
another allocation is required
↓
OOM
↓
error handling interrupts operation
↓
intermediate state survives
↓
request shutdown
↓
cleanup assumes valid state
↓
memory corruptionThe subtle point is that OOM itself is not the vulnerability.
The problem is:
PHP can encounter OOM while internal state is temporarily inconsistent, and a later cleanup path can operate on that state under assumptions that are no longer true.
Why timing the OOM matters
Imagine a 128 MB limit.
An attacker does not conceptually want:
100 MB
↓
huge allocation
↓
OOM100 MB
↓
huge allocation
↓
OOMat an arbitrary location.
The interesting condition is:
Memory almost full
↓
PHP enters interesting operation
↓
state becomes intermediate
↓
next allocation fails
↓
OOM at vulnerable pointMemory almost full
↓
PHP enters interesting operation
↓
state becomes intermediate
↓
next allocation fails
↓
OOM at vulnerable pointThink of a glass:
Capacity: 128 ml
██████████████████░
126 mlCapacity: 128 ml
██████████████████░
126 mlPHP performs:
+0.5 → OK
+0.5 → OK
internal state changes
+3 → OOM+0.5 → OK
+0.5 → OK
internal state changes
+3 → OOMThe OOM occurs while the program is in the sensitive intermediate state.
CVE-2026-109618 in one diagram
Attacker-controlled HTTP input
│
▼
PHP parsing
│
▼
memory pressure
│
▼
partial state
│
▼
allocation fails
│
▼
OOM
│
▼
non-local error handling
│
▼
broken PHP invariant
│
▼
request shutdown
│
▼
unsafe cleanup
│
▼
memory corruption
│
▼
possible primitives
│
▼
RCEAttacker-controlled HTTP input
│
▼
PHP parsing
│
▼
memory pressure
│
▼
partial state
│
▼
allocation fails
│
▼
OOM
│
▼
non-local error handling
│
▼
broken PHP invariant
│
▼
request shutdown
│
▼
unsafe cleanup
│
▼
memory corruption
│
▼
possible primitives
│
▼
RCEPart III — Connecting everything to CVE-2026-109619
The second vulnerability uses a different collection of concepts:
request-scoped memory
+
persistent worker
+
global state
+
free()
+
dangling reference
+
next request
=
Use-After-Freerequest-scoped memory
+
persistent worker
+
global state
+
free()
+
dangling reference
+
next request
=
Use-After-FreeSuppose Request A creates:
Worker global state
GLOBAL
│
▼
0x5000
0x5000
┌────────────────────┐
│ Request A object │
└────────────────────┘Worker global state
GLOBAL
│
▼
0x5000
0x5000
┌────────────────────┐
│ Request A object │
└────────────────────┘Request A finishes.
Its request memory is released:
0x5000
┌────────────────────┐
│ FREE │
└────────────────────┘0x5000
┌────────────────────┐
│ FREE │
└────────────────────┘But imagine the worker still contains:
GLOBAL = 0x5000GLOBAL = 0x5000We now have:
GLOBAL
│
▼
0x5000 [FREE]GLOBAL
│
▼
0x5000 [FREE]This is stale state.
Memory reuse makes it more dangerous
Later:
malloc(...)malloc(...)may reuse that region:
0x5000
┌────────────────────┐
│ New Object B │
└────────────────────┘0x5000
┌────────────────────┐
│ New Object B │
└────────────────────┘Now:
GLOBAL ──────┐
▼
0x5000
▲
│
new pointer ─┘GLOBAL ──────┐
▼
0x5000
▲
│
new pointer ─┘But the old code may interpret that address according to the old object's type and assumptions.
That can lead to memory corruption.
Why the same worker matters
Imagine:
Worker 1
Worker 2
Worker 3Worker 1
Worker 2
Worker 3Request A creates stale state inside:
Worker 2Worker 2If Request B reaches Worker 1, it does not necessarily see Worker 2's process-local state.
The relevant chain therefore looks like:
Request A
↓
Worker 2
↓
producer
↓
stale state
──────────────
Request B
↓
Worker 2
↓
consumer
↓
Use-After-FreeRequest A
↓
Worker 2
↓
producer
↓
stale state
──────────────
Request B
↓
Worker 2
↓
consumer
↓
Use-After-FreeWorker reuse is therefore fundamental to understanding this vulnerability.
CVE-2026-109619 in one diagram
REQUEST A
│
▼
Worker X
│
▼
request memory allocated
│
▼
GLOBAL ──────► Object A
│
▼
abnormal lifecycle
│
▼
Request A memory freed
│
▼
GLOBAL ──────► FREE MEMORY
│
│
stale reference
│
═════════════════╪══════════════════
│
▼
REQUEST B
│
▼
same Worker X
│
▼
stale GLOBAL accessed
│
▼
USE-AFTER-FREE
│
▼
memory corruption
│
▼
possible primitives
│
▼
RCEREQUEST A
│
▼
Worker X
│
▼
request memory allocated
│
▼
GLOBAL ──────► Object A
│
▼
abnormal lifecycle
│
▼
Request A memory freed
│
▼
GLOBAL ──────► FREE MEMORY
│
│
stale reference
│
═════════════════╪══════════════════
│
▼
REQUEST B
│
▼
same Worker X
│
▼
stale GLOBAL accessed
│
▼
USE-AFTER-FREE
│
▼
memory corruption
│
▼
possible primitives
│
▼
RCEPart IV — The fundamental difference
The easiest way to distinguish the vulnerabilities is this:
CVE-2026-109618
Something is used before it has safely finished becoming valid.
Construction:
████████░░░░
│
💥 OOM
│
▼
incomplete state
│
▼
later treated as validConstruction:
████████░░░░
│
💥 OOM
│
▼
incomplete state
│
▼
later treated as validThe central concept is:
partial initialization / broken invariant after OOM.
CVE-2026-109619
Something is used after it has stopped being valid.
Object A
↓
FREE
↓
old reference survives
↓
later access
↓
UAFObject A
↓
FREE
↓
old reference survives
↓
later access
↓
UAFThe central concept is:
object lifetime / stale state / Use-After-Free.
A useful memory trick is:
109618
──────────────
TOO EARLY
PHP treats something as usable
before its state is safely established.
109619
──────────────
TOO LATE
PHP continues using a reference
after its underlying memory lifetime ended.109618
──────────────
TOO EARLY
PHP treats something as usable
before its state is safely established.
109619
──────────────
TOO LATE
PHP continues using a reference
after its underlying memory lifetime ended.Part V — A completely non-technical explanation
Imagine PHP as a hotel.
The PHP worker is one hotel employee.
Each HTTP request is a customer.
CVE-2026-109618 — The unfinished room
The employee prepares Room 42.
He:
- cleans the floor,
- changes part of the bedding,
- starts preparing the bathroom.
Before everything is finished, the room becomes visible to the hotel's internal workflow.
Then:
The hotel suddenly runs out of supplies.
The employee has to stop immediately.
The room is only half prepared, but another internal process later assumes:
"If Room 42 is in this system, it must be completely ready."
It starts performing operations that only make sense for a properly prepared room.
Things go wrong.
That is CVE-2026-109618:
An unexpected OOM interrupts PHP while internal state is incomplete, but later code can operate under assumptions that state was valid.
CVE-2026-109619 — The old hotel key
Yanis receives:
Room 42Room 42The hotel's system stores:
Yanis → Room 42Yanis → Room 42Yanis leaves.
The hotel correctly marks Room 42 as available.
But somebody forgot to remove the old reference:
Yanis → Room 42Yanis → Room 42Alice arrives.
The hotel reuses the available room:
Alice → Room 42Alice → Room 42The old record still says:
Yanis → Room 42Yanis → Room 42If another system trusts that stale record, it now accesses Alice's room while believing it is still dealing with Yanis's room.
That is a Use-After-Free.
In memory:
ptr → 0x5000 [Yanis]
↓
FREE
↓
ptr → 0x5000 [available]
↓
reuse
↓
ptr → 0x5000 [Alice]ptr → 0x5000 [Yanis]
↓
FREE
↓
ptr → 0x5000 [available]
↓
reuse
↓
ptr → 0x5000 [Alice]The address did not necessarily change.
What that address represents changed.
That is the heart of CVE-2026-109619.
Final mental model
If you remember only one diagram from this article, remember this one:
PHP2Shell
│
┌─────────────┴─────────────┐
│ │
▼ ▼
CVE-2026-109618 CVE-2026-109619
OOM Request A
│ │
▼ ▼
partial initialization global reference
│ │
▼ ▼
abrupt error path memory released
│ │
▼ ▼
broken invariant dangling reference
│ │
▼ ▼
unsafe cleanup Request B
│ │
▼ ▼
memory corruption same worker
│ │
│ ▼
│ Use-After-Free
│ │
└──────────────┬─────────────┘
│
▼
memory corruption
│
▼
exploitation primitives
│
▼
RCEPHP2Shell
│
┌─────────────┴─────────────┐
│ │
▼ ▼
CVE-2026-109618 CVE-2026-109619
OOM Request A
│ │
▼ ▼
partial initialization global reference
│ │
▼ ▼
abrupt error path memory released
│ │
▼ ▼
broken invariant dangling reference
│ │
▼ ▼
unsafe cleanup Request B
│ │
▼ ▼
memory corruption same worker
│ │
│ ▼
│ Use-After-Free
│ │
└──────────────┬─────────────┘
│
▼
memory corruption
│
▼
exploitation primitives
│
▼
RCEThe vulnerabilities look complicated because their exploitation involves PHP internals, allocators and heap behavior.
But their underlying ideas are surprisingly simple:
CVE-2026-109618: PHP can be interrupted while building something and later behave as though that thing was safely completed.
CVE-2026-109619: PHP can retain a reference to something after that thing has already been destroyed.
Those two mistakes — using something before it is safely valid and using something after it is no longer valid — are fundamental classes of memory-safety problems.
Understanding them provides much more value than memorizing these two CVE numbers: the same concepts appear repeatedly across browsers, operating systems, network services, interpreters and security vulnerabilities.