[php-src] Issue #22023: PHP 8.5 ZTS Bug Report — Apache worker crash via UAF in `zend_error_zstr_at` when deprecation warning emitted from `OnUpdateUseTransSid` during TSRM thread setup

[email protected] (rschweigardconcept)
Newsgroups php.bugs
Message-ID <[email protected]>
Issue: https://github.com/php/php-src/issues/22023
Author: rschweigardconcept

### Description

# PHP 8.5 ZTS Bug Report — Apache worker crash via UAF in `zend_error_zstr_at` when deprecation warning emitted from `OnUpdateUseTransSid` during TSRM thread setup

**Submit to:** https://github.com/php/php-src/issues/new/choose
(use "Bug Report" template)

---

## Summary

Apache 2.4 (`mpm_winnt`) worker children crash deterministically with
`STATUS_ACCESS_VIOLATION` (`0xC0000005`) inside `php8ts.dll` when **all
three** of the following are configured:

1. PHP 8.5.x ZTS Windows VS17 x86 (verified on **8.5.5** and **8.5.6**)
2. `session.use_trans_sid = 1` (the php.ini-recommended default)
3. Apache `AddDefaultCharset UTF-8` directive active

The crash fires during **TSRM thread setup**, specifically when
`OnUpdateUseTransSid` emits the deprecation warning for `use_trans_sid`
under PHP 8.5. The error-reporting path then performs a `zval_ptr_dtor`
on a zval whose `ecx`-relative refcount slot points to integer `0x0d`
(13) instead of a valid heap address.

The faulting instruction is `sub dword ptr [ecx],1` — `zend_gc_delref()`
inlined into `zend_error_zstr_at` — and the value at `[ecx]` is fully
unmapped (`????????`).

## Faulting context

```
ERROR_CODE: 0xC0000005   STATUS_ACCESS_VIOLATION
WRITE_ADDRESS: 0x0000000d

eax=00001520 ebx=18206000 ecx=0000000d edx=00001520 esi=0f6a1ee0 edi=20201ad0
eip=741d6304 esp=16fbfa30 ebp=16fbfae4

php8ts!zend_gc_delref [inlined in php8ts!zend_error_zstr_at+0x244]:
741d6304 832901          sub     dword ptr [ecx],1    ds:002b:0000000d=????????

FAULTING_SOURCE_FILE:        Zend\zend.c
FAULTING_SOURCE_LINE_NUMBER: 1499
SYMBOL_NAME:                 php8ts!zend_error_zstr_at+244
FAILURE_BUCKET_ID:           NULL_CLASS_PTR_WRITE_c0000005_php8ts.dll!zend_error_zstr_at
```

## Stack trace (fully symbolicated against matching PDB)

```
# inlined:
  php8ts!zend_gc_delref
  php8ts!i_zval_ptr_dtor   +0x11
  php8ts!zval_ptr_dtor     +0x11

# outer frames:
00 php8ts!zend_error_zstr_at        +0x244     ← crash here
01 php8ts!zend_error_zstr            +0x25
02 php8ts!php_verror                 +0x524
03 php8ts!php_error_docref           +0x1b
04 php8ts!OnUpdateUseTransSid        +0x9d     ← deprecation warning emitted here
05 php8ts!zend_ini_refresh_caches    +0x5b
06 php8ts!zend_new_thread_end_handler+0x0c
07 php8ts!allocate_new_resource      +0x121
08 php8ts!ts_resource_ex             +0xee
09 php8apache2_4!?                   +0x2f42
0a libapr_1!apr_table_get            +0x94   (stack unwind unreliable here)
0b libhttpd!ap_sys_privileges_handlers+0xcba
0c libhttpd!ap_run_handler           +0x29
```

The stack from `ts_resource_ex` up through `OnUpdateUseTransSid` is
exactly the per-thread INI cache refresh path. The deprecation warning
must therefore fire **before the worker's zval slots are fully
initialised** — the zval at `ecx` is still the raw IS_LONG(13) it was
seeded with, but `zval_ptr_dtor` treats it as a refcounted value.

## Reproduction

Minimum setup that reproduces:

- Windows Server 2019 (build 17763) — also expected on Win 10/11
- Apache 2.4.66 (Apache Lounge VS18, mpm_winnt with default
  `ThreadsPerChild 250`)
- PHP 8.5.5 *or* 8.5.6 ZTS VS17 x86, loaded as `mod_php` via
  `LoadModule php_module .../php8apache2_4.dll`
- `php.ini`:
  - `session.use_trans_sid = 1` (this is the value shipped in the
    `php.ini-recommended` template!)
- `httpd.conf`:
  - `AddDefaultCharset UTF-8`

Step:

1. Start Apache. Worker children spawn cleanly. So far stable.
2. Issue any HTTP request that triggers a login flow involving session
   start (`session_start()` called from a typical app). In our case
   `POST /index.php` followed by `GET /top.php` was enough.
3. Within seconds of the request completing successfully (HTTP 200 was
   logged), the worker child dies with status `3221225477` and Apache
   restarts it (`AH00428: Parent: child process … exited with status
   3221225477 -- Restarting`).
4. Crash repeats every 1-3 minutes under normal request load.

Removing **any one** of the three trigger conditions stops the crash:

- Setting `session.use_trans_sid = 0` — stable
- Commenting out `AddDefaultCharset UTF-8` — stable
- Downgrading to PHP 8.4.x ZTS — stable

## Workaround

```ini
session.use_trans_sid = 0
```

This avoids triggering the deprecation warning during TSRM thread
setup. (As a bonus, it eliminates a long-standing security hazard —
session IDs in URLs are unsafe regardless.)

## Hypothesis

`OnUpdateUseTransSid` runs from inside `zend_ini_refresh_caches`, which
itself runs from `zend_new_thread_end_handler` during
`allocate_new_resource`. At this very specific point of TSRM init the
per-thread zval slots referenced by the error-formatting code path are
**not yet fully refcounted** — they still carry whatever initial value
the TSRM allocator placed there (looks like a small integer here:
`0x0000000d`).

When `php_error_docref → php_verror → zend_error_zstr → zend_error_zstr_at`
returns and `zval_ptr_dtor` runs on a buffer/arg zval, it dereferences
that integer as a refcounted pointer and crashes at the
`Z_REFCOUNT_DEC` operation.

This is consistent with the fact that the `AddDefaultCharset UTF-8`
directive alone — independent of any PHP-side charset setting — is
enough to make the bug manifest. The charset directive presumably
changes the request handler init ordering enough to push the
`OnUpdateUseTransSid` callback into the unsafe TSRM-init window.

## Crashdump

A 242 MB full crashdump (Windows minidump format, `httpd.exe.8004.dmp`)
is available on request. PDB-symbolicated stack matches the trace
above bit-for-bit, including the line-number resolution to
`Zend/zend.c:1499`.

## Related but distinct issues

- bugs.php.net #81204 — generic "PHP 8 crashes with Access Violation on
  Apache restart" (different stack)
- php/php-src#8250 — Apache & PHP 8.1.4 access violation in `emalloc()`
  (different stack)
- php/php-src#10067 — PHP 8.2.0 access violation in foreach (different
  stack)
- php/php-src#16900 — Apache access violation using INTL (ZTS-related)

The current report is a distinct, reproducible crash that we believe
has not been filed: the unique signature is **OnUpdateUseTransSid →
zend_error_zstr_at → zval_ptr_dtor UAF during TSRM thread setup**.

## Recommendation for fixers

Either:

1. Defer the `session.use_trans_sid` deprecation warning until after
   TSRM thread init is complete (don't emit warnings from INI cache
   refresh during `allocate_new_resource`), or
2. Initialise the temporary zval slots used by the warning-formatter
   path with `IS_NULL` (or properly refcounted strings) before any
   user-visible INI callback can fire.

<!-- Failed to upload "httpd.exe.8004.dmp.zip" -->

### PHP Version

```plain
PHP 8.5.6 (cli) (built: May  6 2026 09:28:51) (ZTS Visual C++ 2022 x86)
Copyright (c) The PHP Group
Built by The PHP Group
Zend Engine v4.5.6, Copyright (c) Zend Technologies
    with Zend OPcache v8.5.6, Copyright (c), by Zend Technologies
```

### Operating System

Windows Server 2019 (build 17763)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.