[php-src] Issue #24493: 8.5.12 upload-progress handler destroys the whole session when a stored object has a property named
__PHP_Incomplete_Class_Name/_Serialized/__PHP_Incomplete_Enum_Case (regression)
| From: | VijhnAustin | Date: | Wed, 21 Oct 2026 18:13:56 +0000 |
| Subject: | [php-src] Issue #24493: 8.5.12 upload-progress handler destroys the whole session when a stored object has a property named __PHP_Incomplete_Class_Name/_Serialized/__PHP_Incomplete_Enum_Case (regression) |
||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253060@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24493
Author: VijhnAustin
### Description
Since commit
31c0ea7d ("Avoid state corruption via bailout",
CVE-2026-109618/109619), the pre-RINIT upload-progress session decoder runs in
incomplete_classes_only mode and rejects any object property named
__PHP_Incomplete_Class_Name, __PHP_Incomplete_Class_Serialized, or
__PHP_Incomplete_Enum_Case (is_incomplete_class_reserved_member() in
ext/standard/var_unserializer.re). The rejection is fail-closed via goto
failure, which aborts the **entire** decode, so the whole session file is deleted
("Failed to decode session object. Session has been destroyed"). These are legal dynamic
property names that PHP's own serialize() / session_encode() can
emit. In 8.5.11 the same session round-trips unchanged.
Repro (default session.upload_progress.*; the session record is produced by
session_encode(), not crafted bytes):
1. A session stores an object with one of those property names — e.g. from a caller-controlled
key:
```php
<?php
session_start();
$o = new stdClass();
$o->{'__PHP_Incomplete_Class_Name'} = 'boo'; // e.g.
$_SESSION['x'] = json_decode($userInput)
$_SESSION['x'] = $o;
```
2. Any later multipart POST that carries the PHPSESSID field and the
PHP_SESSION_UPLOAD_PROGRESS field triggers the pre-RINIT handler for that session.
Resulted in this output (8.5.12 / 8.4.27):
```
Warning: PHP Request Startup: Failed to decode session object. Session has been destroyed in Unknown
on line 0
```
…and the session file is **deleted**.
But I expected this output instead (the 8.5.11 behavior): no warning, the session file kept
byte-identical.
A/B over the three reserved names + a control property (a), records built by
session_encode():
| case | 8.5.11 | 8.5.12 |
|---|---|---|
| control (a) | kept | kept |
| __PHP_Incomplete_Class_Name | kept (66 B) | destroyed |
| __PHP_Incomplete_Class_Serialized | kept (72 B) | destroyed |
| __PHP_Incomplete_Enum_Case | kept (65 B) | destroyed |
Mechanism: in process_nested_object_data(), when
(*var_hash)->incomplete_classes_only is set and the key matches a reserved member,
the code does zval_ptr_dtor(&key); goto failure; — aborting the whole record
rather than dropping just that key. Rejecting the reserved names is necessary (they collide with the
mode's own MAGIC_MEMBER* bookkeeping), but the blast radius could be narrowed:
skip only the offending property (or mangle the key on write, like other internal-key handling) and
keep the rest of the session, plus a regression test asserting the record survives.
Impact: forced session loss (logout / data loss) and log noise for apps that store object properties
under caller-controlled names; otherwise self-inflicted. No memory-safety, confidentiality, or
integrity impact — low severity, filed as a functional regression.
### PHP Version
```plain
PHP 8.5.12 (cli) (built: Oct 20 2026 04:19:56) (NTS Visual C++ 2022 x64)
Copyright (c) The PHP Group
Built by The PHP Group
Zend Engine v4.5.12, Copyright (c) Zend Technologies
```
### Operating System
Windows 11 (code path is OS-independent — ext/standard/var_unserializer.re)