PHP research · Part 3 of 3. Series index · Previous: application code
Source: Negative-results record · version-matrix verifier · fuzz harness · magic-method verifier
Most of the experiments did not complete a chain. Some stopped before a pointer disclosure. Others reached a callback under the wrong lock state, read a record in the wrong format, or needed an address supplied by the lab.
These failures constrain the positive results in Parts 1 and 2. They also determine the next test: changing an address constant cannot fix a missing disclosure, and sharing a Redis key cannot fix an incompatible value format.
A failed test still tells a story.
Inspect the hypothesis and the evidence that stopped each route before a complete chain.
The experiments sought delivery, a useful address disclosure, or a complete application path. Each required different evidence.
Recorded evidence ↗Reduced motion: select steps to change the diagram. Symbolic objects; observed results keep their recorded conditions.
Transcript and recorded profiles
- State the hypothesis. The experiments sought delivery, a useful address disclosure, or a complete application path. Each required different evidence. Evidence
- Run the stated search. Blind fuzzing, source inspection, and selected runtime tests covered different questions. Their counts are not interchangeable. Evidence
- Locate the failure. An absent proof file is not enough. The record identifies a format mismatch, missing leak shape, guard, or external address requirement. Evidence
- Separate assistance from completion. A debug oracle can advance a lab experiment without solving the in-band address source. Rosetta remains emulated coverage. Evidence
- Attach the missing evidence. Select a route to see what remains unresolved. The historical fuzz total lacks a saved per-run transcript. Evidence
- Define the next experiment. Preserve builds, inputs, controls, and receipts in a new rerun. A failure is useful when its stopping point is identifiable. Evidence
- Fuzzing: No usable in-band seed. Historical summary: 4.1 million executions, zero ASAN reports. The count has no preserved per-run transcript and does not measure coverage. Evidence
- Redis: Record rejected before delivery. Key alignment worked. The raw progress array lacked the vars wrapper expected by Roundcube. Evidence
- FPM: Lab-assisted address source. This patched-build profile needed a debug address oracle. A general in-band source was not completed here. Evidence
- Native x86-64: Native address source open. The native seed remained incomplete. The explicit-address Rosetta result is emulated coverage. Evidence
| Test | Stopping point | What remains open |
|---|---|---|
| Fuzzing | No ASAN report or usable in-band seed in the recorded campaign | The aggregate count lacks a preserved per-run transcript. |
| Version matrix | 8.2, 8.3, and 8.5 profiles did not produce the required leak shape | An address calculation cannot repair a missing disclosure. |
| Magic methods | __wakeup() and __unserialize() did not enter the stale context | The destructor path was the positive control in these tests. |
| Redis | The hook's value did not match Roundcube's record format | Other Redis-backed arrangements were not tested. |
| FPM and native x86-64 | The required in-band address source was not completed | Lab-assisted and emulated results stay labelled as such. |
The fuzzing campaign produced no ASAN reports
Executions are not a coverage measurement
Historical aggregate. The historical summary records 4.1 million ASAN executions over 1.24 million inputs across four corpora, with zero ASAN reports. No saved per-run transcript verifies that total.
All recorded cases
- Campaign summary: Historical aggregate. The historical summary records 4.1 million ASAN executions over 1.24 million inputs across four corpora, with zero ASAN reports. No saved per-run transcript verifies that total.
- Coverage limit: Coverage not measured. The mutator used blind variation without coverage guidance or grammar feedback. The graphic does not assign coverage to any engine path.
- Next experiment: Preserve a new run. A fresh bounded campaign must preserve inputs, exact builds, execution logs, and triage output. The public harness supports a new run, not recovery of the missing transcript.
The research summary records 4.1 million ASAN executions over 1.24 million inputs across four corpora.
The campaign summary records zero ASAN reports. Its execution count is historical evidence with two limits.
First, the mutator used blind input variation. It did not use coverage guidance or grammar feedback. Execution count is not a measure of code-path coverage.
Second, no per-run transcript was preserved for the aggregate total. The public harness explains input generation, execution, and triage. It does not let a reader independently verify the historical count from a saved log.
The result should therefore remain attributed to the recorded campaign summary, with the fuzz harness available for inspection.
ASAN silence also does not prove that no in-band address leak exists. A leak may depend on a request sequence, another configuration, or behaviour that this instrumentation and corpus did not expose.
The campaign did not supply a usable seed. That was enough to change the next experiment.
The version matrix stopped before the address calculation
The leak route stops before the offsets
Six selected positives. The core-chain matrix records positive results on 8.4.10, .20, .23, .24, .25, and .26, with its stated controls. These are selected builds, not an entire-branch guarantee.
All recorded cases
- Selected 8.4 builds: Six selected positives. The core-chain matrix records positive results on 8.4.10, .20, .23, .24, .25, and .26, with its stated controls. These are selected builds, not an entire-branch guarantee.
- Other branches: Leak shape missing. 8.2.34, 8.3.35, and 8.5.11 did not supply the required disclosure shape. Recalculating an offset cannot supply the missing pointer disclosure.
- Patched negative: Original route stopped. The original leak route has a patched negative on 8.4.27. Part 1 uses a separate destructor delivery path on that release; the two results are compatible.
Six selected PHP 8.4 builds passed the core-chain matrix: a positive result, a patched negative, and a wrong-address control.
The tested versions were 8.4.10, 8.4.20, 8.4.23, 8.4.24, 8.4.25, and 8.4.26. Version 8.4.1 was excluded because its disclosure did not provide usable pointers.
The other tested branches stopped at the leak shape:
| Build | Recorded result |
|---|---|
| 8.2.34 | Normal session identifier returned instead of the required disclosure |
| 8.3.35 | Same leak-shape failure |
| 8.5.11 | No usable disclosure; the tested OOM path also aborted the worker |
The offsets had already been extracted for those builds. Recalculating them would not repair a path that never supplied a pointer.
This matrix concerns the original core-chain leak route. Its patched negative on 8.4.27 does not contradict Part 1's separate var_destroy() delivery on that release.
The status matrix keeps the positive, negative, bias, and skipped rows separate. When the positive leg failed, the later controls were not promoted into evidence of a complete chain.
The magic methods reached different parser states
__wakeup(), __unserialize(), and __destruct() all looked relevant because they can run while an object is being decoded or cleaned up.
The lock behaviour made them different tests.
Across eight tested combinations on 8.4.27 and 8.5.12, __wakeup() and __unserialize() ran but did not reproduce the required stale-context re-entry. Their branches held the serialization lock, so the nested parser obtained a fresh context.
The destructor positive control reached the vulnerable state.
That comparison supported a specific conclusion: the required re-entry occurred through the unlocked destructor path in these tests. It did not support treating every magic method as interchangeable.
A throwing wakeup method added another failure mode by suppressing later destructor execution. A graph that looked valid on paper could therefore stop before the needed call.
The magic-method test record contains the negative matrix and positive control.
Redis rejected a record format
The Roundcube experiment tested a clear hypothesis: if the upload hook and Roundcube used the same Redis key, stock code might perform both the switch and the read.
Aligning the key was insufficient.
Roundcube expected a wrapper with a vars field. The hook's progress array did not contain that field. The decoder therefore did not receive the intended bytes.
The record-format check identifies the stopping point before destructor execution. An absent proof file alone would not locate that failure.
It ruled out the proposed profile under the tested conditions. It did not rule out every application path that could write a different Redis value, nor every possible concurrent layout.
The FPM gap belongs to this lab
This repository did not complete a general in-band address leak for its FPM chain.
One instrumented profile reached a later stage using a debug oracle to obtain the needed address. That establishes a lab-assisted result. It does not establish the same result without the oracle.
Other teams reported PHP2Shell results that include FPM. PHP's stale-state advisory says session-lifecycle RCE was validated on mod_php and FPM; Calif's report describes its own application results. Those reports address the October CVE paths. They do not fill the missing in-band address source for our patched-build var_destroy() profile.
For this patched-build profile, the FPM address source remains lab-assisted. The cited independent results establish their own paths; an in-band rerun is still required for ours.
The arm64 search does not establish native x86-64 coverage
The arm64 lab could search a limited address window. Wrong guesses killed workers, and the parent process replaced them. The recorded runs eventually found an address that served the tested profile.
That method had a measurable availability cost: unsuccessful attempts caused worker failures.
The native x86-64 configuration had a much larger search space. The work did not produce a general seed there. The explicit-address result under Rosetta remains an emulated result.
The missing experiment is a native x86-64 run with an independently obtained address source, not a larger attempt count. Partial search results cannot establish that result.
The architecture notes describe that boundary and the request-specific address constants.
What remains after these tests
The record supports several different outcomes:
- →A fixed command ran under a stated configuration.
- →ASAN confirmed a memory error.
- →A destructor path existed but an application guard blocked it.
- →A driver received a record in the wrong format.
- →A search failed to produce the address needed for the next step.
- →A result was documented, but its full reproduction package was not preserved.
The public open-work list names the next test and the evidence required to close each gap. A rerun that fails is still a result if its build, configuration, and stopping point are recorded.
The source review, controlled reproductions, and stated limits in this investigation are also part of our security research and application testing work.