[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)