Skip to content

Commit ee01438

Browse files
committed
Fix GH-20352: UAF in php_output_handler_free via re-entrant ob_start() during error deactivation
The problem is that the code is doing `php_output_handler_free` in a loop on the output stack, but prior to freeing the pointer on the stack in `php_output_handler_free` it calls `php_output_handler_dtor` which can run user code that reallocates the stack, resulting in a dangling pointer freed by php_output_handler_free. Furthermore, OG(active) is set when creating a new output handler, but the loop is supposed to clean up all handlers, so OG(active) must be reset as well. Closes GH-20356.
1 parent 983be08 commit ee01438

File tree

3 files changed

+35
-4
lines changed

3 files changed

+35
-4
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ PHP NEWS
88
. Fixed bug GH-20695 (Assertion failure in normalize_value() when parsing
99
malformed INI input via parse_ini_string()). (ndossche)
1010
. Fixed bug GH-20714 (Uncatchable exception thrown in generator). (ilutov)
11+
. Fixed bug GH-20352 (UAF in php_output_handler_free via re-entrant
12+
ob_start() during error deactivation). (ndossche)
1113

1214
- Bz2:
1315
. Fixed bug GH-20620 (bzcompress overflow on large source size).

main/output.c

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,12 @@ PHPAPI void php_output_deactivate(void)
188188
/* release all output handlers */
189189
if (OG(handlers).elements) {
190190
while ((handler = zend_stack_top(&OG(handlers)))) {
191-
php_output_handler_free(handler);
192191
zend_stack_del_top(&OG(handlers));
192+
/* It's possible to start a new output handler and mark it as active,
193+
* however this loop will destroy all active handlers. */
194+
OG(active) = NULL;
195+
ZEND_ASSERT(OG(running) == NULL && "output is deactivated therefore running should stay NULL");
196+
php_output_handler_free(handler);
193197
}
194198
}
195199
zend_stack_destroy(&OG(handlers));
@@ -719,10 +723,11 @@ PHPAPI void php_output_handler_dtor(php_output_handler *handler)
719723
* Destroy and free an output handler */
720724
PHPAPI void php_output_handler_free(php_output_handler **h)
721725
{
722-
if (*h) {
723-
php_output_handler_dtor(*h);
724-
efree(*h);
726+
php_output_handler *handler = *h;
727+
if (handler) {
725728
*h = NULL;
729+
php_output_handler_dtor(handler);
730+
efree(handler);
726731
}
727732
}
728733
/* }}} */

tests/output/gh20352.phpt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--TEST--
2+
GH-20352 (UAF in php_output_handler_free via re-entrant ob_start() during error deactivation)
3+
--FILE--
4+
<?php
5+
class Test {
6+
public function __destruct() {
7+
// Spray output stack
8+
for ($i = 0; $i < 1000; $i++)
9+
ob_start(static function() {});
10+
}
11+
12+
public function __invoke($x) {
13+
// Trigger php_output_deactivate() through forbidden operation
14+
ob_start('foo');
15+
return $x;
16+
}
17+
}
18+
19+
ob_start(new Test, 1);
20+
21+
echo "trigger bug";
22+
?>
23+
--EXPECTF--
24+
Fatal error: ob_start(): Cannot use output buffering in output buffering display handlers in %s on line %d

0 commit comments

Comments
 (0)