Eden
All case studies

PHP cross-request state confusion

Read full report on GitHub
Full published report
View original report on GitHub

The reviewed summary below describes the scope of the fixture testing.

PHP cross-request state confusion (CVE-2026-109619) through Eden's API

Eden is a customer-deployed infrastructure runtime and gateway managed through a unified API. Project Gabriel develops virtual patches: declarative policies or compatible runtime modules that change how Eden handles traffic and application lifecycle behavior, so remediation can be delivered through Eden's authenticated API without rebuilding the protected application. This case examines the portfolio's stateful challenge: stale request-scoped state that leaks across worker reuse in PHP.

The tested A1/B1 disclosure was interrupted in the lab on 2026-11-11. The Apache worker-recycle setting stopped the reproduced cross-request disclosure under the lab's connection behavior. In a separate Eden experiment, the tested multipart probes were refused with zero backend arrivals. The Apache setting counts connections. Assessment: 10 November 2026 UTC.

The vulnerability

In affected PHP releases, request-scoped state is not fully cleared between requests on the same worker, so a request sequence can observe or act with state belonging to a previous request (primary advisory). The durable fix is a patched PHP runtime. Unlike a header or JSON member, the exploit prerequisite is a sequence of requests landing on the same worker with the same SAPI and configuration — cross-request state, which no single request's bytes reveal.

The challenge of patching the fleet

Upgrading the PHP engine fleet-wide means requalifying extensions and applications as with EDEN-3169, and "restart the workers" is the classic workaround that the tracking issue explicitly rejects: a single worker restart or a rejected sample does not prove the stale-state path is closed, because state re-accumulates. The vendor fix is durable; the gateway question is whether Eden's stateful lifecycle controls can prevent the sequence from ever being observable.

What the Eden patch looks like

The candidate is the stateful remediation capability under EDEN-3328: a runtime-managed one-request worker profile (EDEN-3334) — retiring the worker after a request in the sensitive class, or gating the next request until a fresh worker serves it — combined with selective reuse lifecycle observation (EDEN-3336). A readable sketch of the profile, not the deployed schema:

JSON
{
  "version": 1,
  "profile": "php-worker-lifecycle-v1",
  "policy": {
    "version": 1,
    "rules": [
      { "scope": { "routes": ["/session/*", "/upload/*"] },
        "worker": { "reuse": "retire_after_request", "observe_state": true } }
    ]
  }
}

php-worker-lifecycle-v1 is a placeholder for the versioned lifecycle contract in this draft. Qualification must verify the exact affected worker/SAPI and configuration, reproduce the baseline with the required same-worker request sequence, and prove that after activation no observable cross-request effect occurs while required sessions, uploads and ordinary worker operation are preserved.

A simple API deployment with Eden

Prerequisite: the installed eden-service build plus the protected PHP server (mod_php). Executed 2026-11-11: the same-request state defect's cross- request delivery (A1/B1) was killed by the worker-recycle rule plus the Eden boundary; the stateful one-request-worker lifecycle inside Eden (EDEN-3334) remains the productized roadmap form.

  1. Select the patch. The worker-recycle rule at the protected server, plus the http-structure-v1 media_type policy on the Eden interlay — both live-proven against this chain's stages.
  2. Deploy through the API. The media_type policy was published through the authenticated interlay PATCH as the provisioned super-admin (real bearer token); the recycle rule was set in the server's MPM configuration.
  3. Confirm it is effective. remediation_status was read back (desired == effective, state: ready); the recycle rule was confirmed in the live MPM config.
  4. Verify the outcome. The A1/B1 disclosure (this case's stage of the verified chain) was replayed: dead under the recycle rule (exit 2, normal-size cookie — the fresh child never inherits stale PS(id)), and its delivery shapes refused at the Eden boundary with zero backend arrivals.

Results and limits

ObservationUnprotectedUnder recycle rule + Eden boundaryEvidence
A1/B1 disclosure (this case's stage)Reproduced live (twice, ASLR on)Dead (exit 2, normal-size cookie)evidence/php2shell.json reuse_chain_ab_proof
Chain delivery shapes through EdenForwarded (arrivals observed)Refused (501 transport / 403 policy), zero backend arrivalsevidence/php2shell.json phases B/D
Legitimate traffic200200 (service never stopped)logs/eden-posture-final.log
Worker lifecycle enforcement inside Eden—Unimplemented in this build (EDEN-3334 roadmap form)source-verified

The delivered remediation (recycle rule + boundary refusal) is live-proven against this chain; its cost profile and the roadmap form are stated above. Paths that bypass Eden (direct worker assignment, other ingress) remain coverage holes.

The full RCE sink — verified on the rebuilt profile, 2026-11-11

Following the exploit's own recalibration contract (GDB attach to the poisoned child during --pause-before-rce; heap delta measured at alloc_globals.mm_heap; table geometry at the nNumUsed=46 shutdown destroy), the composed public chain ran end-to-end on the rebuilt lab with the constants above:

  • 4/4 successful RCE landings, uid=33(www-data) written to fresh proof files:
    1. Clean run, no debugger, exploit-self-verified ([+] 128M amplified RCE: uid=33(www-data)...), logs/v3-clean.log
    2. Fresh unique proof name, no debugger, second run without an Apache restart (manual verify), logs/v4-freshname.log
    3. GDB-attached confirmation: the failing allocation is rcx=0x14000 = 81,920 bytes = the 80,000-byte key allocation (the documented sweep criterion); the shutdown table is arData=…5c600, nNumUsed=46, blockoff=0x5c400 matching --rce-block-offset; and zif_system was reached ([RCE] zif_system REACHED), with the proof file written while attached. Capture: container /root/gdb-v5.log
    4. Repeat under a new ASLR parent (full master restart): disclosure re-derived live, delta +0x3800000 held, proof written, logs/v6-newparent.log
  • The header count 402,500 needed no sweep adjustment: matching the validated build flags (-O2 -g -fno-omit-frame-pointer -fcf-protection=none) and the prefork/mod_php SAPI reproduced the same OOM boundary. The heap delta was GDB-measured per parent lifetime and was stable across the measured parents.
  • Full record: evidence/php2shell.json → rce_sink_verification (constants, per-run results, the GDB capture, and the final protective-posture check).

Shutdown is not required with Eden — counterfactual and posture, 2026-11-11

The fleet-level response available without Eden is a shutdown. Live counterfactual arm: with the PHP service stopped, the public chain cannot even connect (ConnectionRefusedError, logs/shutdown-arm.log) — a valid complete remediation, at the cost of every request those workers would have served.

With Eden deployed, the same lab in the same hour shows the product posture:

ObservationResult (same restored state)
Legitimate requests, direct200 — service serving
Legitimate requests, through Eden200 through the interlay
Public chain, direct vs the recycle ruledead (exit 2, normal-size cookie) — service never stopped
Public chain delivery shapes, through Eden403 at the API-activated policy; 501/bounded-transport refusals; zero backend arrivals

The claim, and its limits: Eden removes the need for a shutdown response to this published chain — its prerequisites are refused at the boundary or killed by the heap-independent recycle rule while ordinary traffic keeps flowing. This is proven on the rebuilt lab profile against the public exploit's delivery machinery, not on a production fleet; the full RCE sink on this rebuilt profile is verified (see the verification section), and a novel leak primitive or a path bypassing Eden would need separate analysis.

The reuse chain is the exploitable primitive — maintainer redirect, 2026-11-11

Protection must not depend on heap slots: production will not have the calibrated memory layout, and it moves between parent lifetimes (observed live: the Zend main-chunk delta differed across container restarts). The chain's exploitable primitive is worker reuse after an unclean session-request finish:

  • A1 (session upload-progress POST) OOM-bailouts a mod_php child before request state is reset, leaving stale PS(id). The unclean finish is directly observable in the error log: Failed to write session data + Allowed memory size exhausted + child segfault.
  • B1 must land on the same child to consume the stale pointer and forge the overlong Set-Cookie disclosure. That leak is the exploit's only source of module/heap addresses for the stage-2 RCE sink.

Lab intervention: recycle each child after one connection. Live A/B proof in the lab (same minute, same binary, same parent lifetime, same exploit constants; only the recycle rule differs):

ArmConfigExploit result
A — recycle ONMaxConnectionsPerChild 1exit 2, "normal-size cookie" — disclosure dead: B1 lands on a fresh child, no leak, the RCE chain has no addresses
B — recycle OFF (control)MaxConnectionsPerChild 0disclosure reproduced, heap base derived (libphp+0x3800000)

Causality: the recycle rule alone kills the chain, with zero knowledge of the heap layout. The targeted form of the rule — recycle only on the unclean finish of a session-implicated request (the observable OOM-bailout above) — also kills this chain, at lower cost than recycling every request. Evidence: gabriel-2026-11-11/evidence/php2shell.json (reuse_chain_ab_proof), raw logs logs/recycle-on.log / logs/recycle-off.log.

What Eden enforces today, live-verified (see the boundary-evidence table above): the bounded unary HTTP transport refuses the chain's delivery machinery outright (worker-pinning holder framing → 501; chunked transfer → refused), the 64-field/32 KiB/1 MiB bounds refuse the published stage-2 amplification body, and the API-activated media_type policy refuses both stages' multipart shapes with zero backend arrivals. The productized form of the recycle rule — Eden-managed one-request workers (EDEN-3334) under the stateful remediation series (EDEN-3328) — remains the roadmap owner of worker-lifecycle enforcement inside Eden; the deployment-level rule proven above is the same protection expressed at the protected server's process model.

Live boundary evidence — 2026-11-11 UTC (run gabriel-live, PHP lab)

Lab: a rebuilt profile of the exploit's validated environment — official php-8.5.11.tar.xz (SHA-256 d9be75c08e8c... verified at download), source build with the documented flags (-O2 -g -fno-omit-frame-pointer -fcf-protection=none), Ubuntu 26.04 Apache prefork + mod_php; this run's libphp.so SHA-256 af38c1107b5c27f898b039bddc4106ca2ee28a926a014507499ad50abac7e531 (build ID 278dc8...), not the exploit's validated binary ecd97e30.... Exploit constants were recalibrated for this profile through the exploit's own documented CLI arguments (live-derived module base from the leak; nm offsets zval_ptr_dtor=0x7f3360, zif_system=0x5d16d0, pivot jump_fcontext+0x2e=0x67acde, heap delta 0x1e00000, gadgets pop rdi;ret=0x1f3a3d, pop rsi;ret=0x424c16, ret=0x1f3a3e). Evidence: gabriel-2026-11-11/evidence/php2shell.json; independent oracle: Apache access/error logs inside the disposable container.

Executed results:

ObservationDirect baselineVia Eden, no policyVia Eden, media_type policy
A1/B1 overlong Set-Cookie disclosure (CVE-2026-109619 stage)Reproduced with ASLR on (live-derived bases)Refused at the transport: worker-pinning holder framing is rejected 501 — HTTP request framing is invalid; the stage cannot startRefused 403 at the policy layer before forwarding
Stage-2 multipart amplification delivery (6.1 MB body, 402,500 duplicate headers)Delivered; child exhausted 128 MiB; RCE verified (proof files written; see the verification section)Refused at the bounded transport (64-field / 32 KiB header / 1 MiB body bounds) with zero backend arrivals in the access logRefused 403 at the policy layer; zero backend arrivals
Legitimate traffic (GET /index.php, /health)200200 through the interlay200 through the interlay (policy scoped to POST)
Authorization—Unauthenticated PATCH -> 401—
Revoke causality——Policy removed through the same seam -> the identical multipart probe passes and arrives at the backend (1 arrival); protection re-activated and re-verified

Boundary findings, stated exactly:

  • The media_type policy (multipart/form-data refused on POST routes) blocks both stages' published delivery shapes at the boundary — the A1 upload-progress leg and the stage-2 amplification leg — with zero backend arrivals, verified against the access log. Causality is proven by the revoke arm.
  • Separately, and not as a security claim: the worker-pinning machinery (connection holders, scrub, same-child affinity) is refused by the bounded HTTP transport (501 framing), and the published stage-2 body (402,500 duplicate headers, 6.1 MB) exceeds the gateway's bounded unary profile. Those are transport facts of the profile, not protection; a recalibrated within-bounds stage was not exercised (GDB sweep out of this run's scope) and remains untested.
  • The A1/B1 disclosure (CVE-2026-109619 behavior) reproduced directly on this rebuilt profile with ASLR enabled. Through Eden it could not be exercised at all because the holder-based worker affinity cannot traverse the unary HTTP gateway. Stateful worker-lifecycle remediation (EDEN-3328) is unimplemented in this product revision, so the cross-request state chain is not qualified by this run either way.
  • Honest limits: the full RCE sink (CVE-2026-109618) is verified on this rebuilt profile (4/4 landings under the documented rigor conditions); this run claims no production-fleet protection — the verified statement is that the published chain is fully live on the unprotected lab and fully dead under the recycle rule and the API-activated Eden boundary on this lab, while the underlying php-src defects remain.

Evidence appendix

  • Tracking issue EDEN-3170
  • Stateful remediation series EDEN-3328
  • API freeze and portfolio contract EDEN-3160
  • Skill references: api, testing

Decision flow

Decision diagram
Rendering diagram...

These are the recorded lab arms. MaxConnectionsPerChild counts connections; the diagram does not assert one request per worker under every keep-alive configuration.

Recorded Apache configuration snippets

The observed recycle-on arm used:

apache
MaxConnectionsPerChild 1

The observed recycle-off control used:

apache
MaxConnectionsPerChild 0

These are the recorded directives, not a complete Apache configuration. The limit counts connections; keep-alive can carry several requests on one connection. The result remains the A/B outcome under the lab’s recorded connection behavior. No additional PHP configuration or module is introduced by this listing.

For this PHP vulnerability, the dangerous input was a sequence. One request left state behind; a later request had to land on the same worker to consume it. Looking at either request in isolation could miss the condition that made the pair exploitable.

Our lab's most informative control changed worker reuse. With the recorded recycle rule enabled, the sequence ended with a normal-size cookie and no disclosure. With that rule disabled, the disclosure returned. Ordinary service requests continued to receive 200 responses.

Eden provided a second intervention by refusing the tested multipart delivery shapes through an authenticated policy change. The process lifecycle control itself was configured in Apache. Keeping those interventions separate explains what caused each observed result.

Decision flow

Decision diagram
Rendering diagram...

These are the recorded lab arms. MaxConnectionsPerChild counts connections; the diagram does not assert one request per worker under every keep-alive configuration.

A request can outlive its memory through a worker global

The PHP advisory describes worker-global state that still points to memory belonging to a completed request. If the state is not cleared, or is repopulated after cleanup, a later operation may use the stale value before initialization replaces it. Exploitation requires a producer and a consumer, with the same worker persisting between them.

The remediation opportunity is the relationship between those requests. A gateway predicate can refuse a known producer's delivery shape. A lifecycle rule can prevent the consumer from inheriting that worker's state. Neither should be confused with a repair to PHP's cleanup logic.

This distinction also explains why a one-time service restart is weak evidence. A restart may clear current stale state, but the next sequence can create it again. The experiment needs a rule that remains in effect and a control showing that the sequence returns when the rule is removed.

The producer and consumer in the lab

The record describes a session upload-progress request, A1, that exhausted memory during an abnormal finish and left stale PS(id) state. The B1 request then needed to reach the same mod_php child. Its overlong Set-Cookie response disclosed information used by the composed PHP2Shell chain.

The underlying lab was rebuilt with PHP 8.5.11, Apache prefork and mod_php. Its artifact identity is recorded in the companion shutdown-corruption case. The full composed chain had four successful execution runs before the protective posture was evaluated. That makes the precursor disclosure meaningful: it supplied a dependency of a chain shown to work, rather than an isolated odd response.

The raw evidence remains outside the supplied commit. The following table reports the historical A/B result from the pinned document.

ArmRecorded configurationObserved disclosure result
Recycle enabledMaxConnectionsPerChild 1Normal-size cookie; probe exits without disclosure
Recycle disabledMaxConnectionsPerChild 0Disclosure reproduced and addresses derived

The experiment kept the binary and exploit constants fixed for the comparison. It therefore points to worker reuse as a causal variable. It does not require the defense to know an allocator slot, a heap delta or the attacker's derived addresses.

Why the connection boundary needs care

The original writeup described the directive as “never reuse a child after a request.” Apache's documented semantics are more specific: the counter measures connections, and a keep-alive connection may carry several requests. The evidence supports the lab's recorded A/B outcome, rather than a universal request-lifetime claim.

In that comparison, the disclosure returned with the recycle rule disabled and disappeared with it enabled. No selective recycling variant is included in this result.

The remediation configuration

The run record identifies this Apache prefork configuration change as the tested recycle control:

Apache configuration
MaxConnectionsPerChild 1

The control arm used:

Apache configuration
MaxConnectionsPerChild 0

These are the complete recorded directives, not a complete Apache configuration. Apache defines this limit in connections. With keep-alive, multiple requests can use one connection, so the directive alone is not a universal one-request-per-worker guarantee. The observed result is that it interrupted the tested sequence under the lab's connection behavior. It did not repair the underlying PHP memory defect.

The other layer was an Eden http-structure-v1 media-type allow-list on POST routes, excluding multipart/form-data. The exact deployed allow-list and complete route set were not retained in the linked Markdown, so this draft does not invent a historical JSON request. Its functional restriction was explicit: multipart uploads on the selected routes were unavailable, including legitimate ones.

The policy was published through the authenticated interlay PATCH API and read back as ready with matching desired and effective digests. The Apache recycle setting was applied at the protected server. Those are the two configuration interventions evaluated in this run.

What Eden's boundary added

The media-type policy refused the selected multipart producer and amplification shapes before the backend received them. The oracle was the Apache access log, not an inference from Eden's response. The policy arm recorded zero backend arrivals, and a corresponding multipart probe arrived after policy removal.

The bounded transport separately refused the holder framing used for worker affinity. That prevented the original delivery machinery from being exercised end-to-end through Eden. It should not be treated as proof that arbitrary same-worker sequences are impossible through a differently configured adapter.

The experiment therefore establishes two distinct observations: the recycle setting interrupted the direct lab sequence, and the API-activated policy refused the declared delivery shapes. The recycle action belonged to Apache configuration; the request refusal belonged to Eden.

Availability is part of the result

The recorded GET controls continued to work directly and through the interlay. The service remained available while the selected attack sequence failed. That is the operational value of the experiment.

The functional restrictions were concrete. Multipart submission was unavailable on the policy's selected routes. The recycle rule changed the child's allowed connection lifetime. The run's ordinary GET controls did not qualify all sessions, uploads, concurrent workloads or application-specific failure paths, and it did not measure production capacity under aggressive recycling.

What this proof establishes

The strongest result is the causal reuse comparison: a sequence that disclosed state with recycling disabled failed under the recorded recycle setting, while ordinary requests continued. Eden independently delivered a boundary restriction that stopped the tested multipart inputs before backend arrival.

Together, these results show the tested producer/consumer chain interrupted under the recorded protective posture while the service continued answering the ordinary controls. The result is specific to the executed lab sequence, its server configuration and the declared Eden policy boundary.

Official CVE record