The October 21 PHP security releases closed two PHP2Shell paths. We asked a different question: could a session record still reach the older destructor re-entry bug on those release builds?
On PHP 8.4.27 and 8.5.12, the answer in our local labs was yes. A serializer mismatch delivered the record to PHP's own decoder; delayed cleanup invoked a destructor; nested parsing reused freed bookkeeping. A separate stock-allocator test reached a fixed command. The path required enabled upload progress, a runtime serializer switch, a suitable loaded destructor, memory placement, and an address source. Those conditions define the result.
This series follows that chain from the engine to application code, then follows the experiments that failed. The October 21 release notice is the baseline, not evidence that a default PHP installation exposes this path.
The underlying bug and composition were already public. Dinosn published the destructor re-entry bug; Calif published related unserializer research; and ACD421 published the four-stage composition. Our contribution is a measured port to the named release builds, controls that stop the tested chain, application profiles, and documented failures. The prior-art correction records why we narrowed the original novelty claim.
One record. Several boundaries.
Follow a stored record across a format boundary, into parser cleanup, and out to the limits of the application results.
The upload-progress hook writes a session record using the configured serializer. Application routing has not started.
Recorded evidence ↗Read this chapter ↗Reduced motion: select steps to change the diagram. Symbolic objects; observed results keep their recorded conditions.
Transcript and recorded profiles
- Before application code. The upload-progress hook writes a session record using the configured serializer. Application routing has not started. Evidence · Article
- The record stays; the reader changes. A runtime switch selects the legacy php reader. With the mismatch present, part of the same record can reach the unserializer. Evidence · Article
- Objects bring cleanup work. The parser tracks created values and delayed cleanup. A suitable loaded destructor is an additional condition. Evidence · Article
- Lifetime ends before callbacks finish. The reviewed cleanup releases bookkeeping before the destructor walk finishes. Nested parsing can follow the stale context. Evidence · Article
- Two different experiments. ASAN establishes an invalid read. A separate stock-allocator profile needed gadget, placement, and address conditions to reach a fixed command. Evidence · Article
- The application sets the boundary. Drupal required configuration; Roundcube required a lab plugin. Joomla and our WordPress candidate stopped earlier. FPM and native address sources remain open here. Evidence · Article
- Handler mismatch: Tested delivery path. Configured producer and runtime legacy reader; enabled upload progress and suitable loaded destructor. Evidence
- No runtime switch: Delivery stopped. Removing the runtime handler switch stopped the tested delivery. Other PHP paths are outside this control. Evidence
Read the research
- →The destructor path that survived the patch follows the engine path from the session decoder to the memory error and the local fixed-command result.
- →Following the chain into application code tests the conditions in Drupal, Joomla, Roundcube, and WordPress. It separates a completed lab profile from a loaded gadget or a blocked path.
- →What the failed tests taught us records the version, Redis, FPM, architecture, and fuzzing limits.
The earlier session-decoder technical record preserves the detailed test sequence, payload shapes, and controls. Start with the three-part series for the current account. One byte past the buffer covers a separate SJIS-mac mbstring finding; it is outside this session chain.
Results and reproduction boundaries
| Observation | Public evidence | Boundary |
|---|---|---|
An instrumented run read freed memory during var_destroy() cleanup | Patch-gap record and minimal verifier | An ASAN report establishes the memory error, not command execution. |
| The tested session chain reached a fixed command on 8.4.27 and 8.5.12 | Session-chain verifier and loopback lab | The result depends on the named configuration, gadget, memory placement, and address source. |
| Application code changed which part of the chain was reachable | Application results, Drupal lab, and Roundcube lab | Drupal was configuration-gated; Roundcube was plugin-assisted; Joomla stopped at the memory error; our WordPress candidate path was blocked. |
| Several proposed routes stopped before a complete chain | Negative-results record, version verifier, and fuzz harness | The historical fuzz count has no saved per-run transcript; our FPM and native x86-64 address sources remain incomplete. |
The php2shell-hunt repository contains the public research files. Links in the table point to the reviewed 56f0331 commit so the evidence stays identifiable. All described command tests ran in local containers with the fixed command id>/tmp/vd 2>&1.
Upstream status on November 2, 2026
- →PHP fixed CVE-2026-109618 and CVE-2026-109619 in its October 21 releases. Wiz, Calif, and OpenAI describe their research and the coordinated response. Their application results concern those October paths.
- →PHP's October 23 advisory describes the narrow configuration required for CVE-2026-115560 and says a full advisory will follow in a regular release.
- →PHP closed the main
var_destroy()report after an October 30 source change. A related SPL entry-point report and proposed change remain open. We have not tested a later release containing these changes. - →The mbstring companion is public. Its private vendor report is not in the research repository; vendor acknowledgement and a fix are not established here.
Open work
The next experiments address the missing parts of the record. Each row states what must be preserved for an independent review. Tests run in owned, loopback-only environments.
| Next step | Evidence needed to close it |
|---|---|
Recheck the var_destroy() path after PHP publishes a release with the upstream changes | Record the exact PHP build and source revision; rerun the minimal memory-error test, the session profile, and their controls. Publish positive and negative results. |
| Preserve a fresh fuzzing record | Run a bounded campaign with versioned inputs, command lines, ASAN output, and a run manifest. Keep the historical 4.1 million-execution figure labelled as a summary without a preserved transcript. |
| Reproduce the application profiles from clean lab setup | Record container digests, configuration, fixed-command result, and negative controls for Drupal and Roundcube. Seek an independent rerun when possible. |
| Resolve the FPM and native x86-64 evidence gaps | State the result of controlled local tests, including failures. Do not promote the lab-assisted FPM result or Rosetta run into a general native result. |
| Track the separate mbstring report | Record vendor response, any fix and regression test, and the first released build that includes it. Keep its status separate from this session research. |
Completion requires a dated result and a direct evidence link, including when a rerun fails.
The source review, controlled reproductions, and stated limits in this investigation are also part of our security research and application testing work. For an uncertain technical question, start with software R&D and prototyping.