Commit 39a230ccda0 for php.net
commit 39a230ccda052255249bf1ecda6d28f8daf6da2b
Author: Jakub Zelenka <bukka@php.net>
Date: Tue Sep 15 19:28:26 2026 +0200
main/streams: Pair refused stream error operations with a no-op end
When the depth limit refuses a begin, the matching end popped an unrelated
operation and desynchronized the stack.
diff --git a/ext/standard/tests/streams/stream_errors_operation_depth_limit.phpt b/ext/standard/tests/streams/stream_errors_operation_depth_limit.phpt
new file mode 100644
index 00000000000..1eae9c5fd31
--- /dev/null
+++ b/ext/standard/tests/streams/stream_errors_operation_depth_limit.phpt
@@ -0,0 +1,72 @@
+--TEST--
+Stream errors: operations refused by the depth limit keep the stack consistent
+--INI--
+zend.max_allowed_stack_size=-1
+--FILE--
+<?php
+class RecursiveStream
+{
+ public $context;
+ public static int $depth = 0;
+ private int $reads = 0;
+
+ public function stream_open($path, $mode, $options, &$openedPath): bool
+ {
+ return true;
+ }
+
+ public function stream_read(int $count): string
+ {
+ $outermost = self::$depth === 0;
+ if (++self::$depth < 1010) {
+ $f = fopen('recursive-stream://x', 'r', false, $GLOBALS['ctx']);
+ fread($f, 1);
+ fclose($f);
+ }
+ self::$depth--;
+ if (!$outermost) {
+ return 'x';
+ }
+ return ++$this->reads <= 2 ? str_repeat('A', $count + 1) : '';
+ }
+
+ public function stream_eof(): bool
+ {
+ return self::$depth > 0 || $this->reads >= 3;
+ }
+
+ public function stream_stat(): array
+ {
+ return [];
+ }
+}
+
+stream_wrapper_register('recursive-stream', RecursiveStream::class);
+
+$depthWarnings = 0;
+set_error_handler(static function (int $severity, string $message) use (&$depthWarnings): bool {
+ if (str_contains($message, 'depth exceeded')) {
+ $depthWarnings++;
+ return true;
+ }
+ return false;
+});
+
+$ctx = stream_context_create(['stream' => [
+ 'error_mode' => StreamErrorMode::Silent,
+ 'error_store' => StreamErrorStore::All,
+ 'error_handler' => static function (array $errors): void {
+ echo "handler: " . count($errors) . " error(s)\n";
+ },
+]]);
+
+$stream = fopen('recursive-stream://x', 'r', false, $ctx);
+var_dump(strlen(stream_get_contents($stream)));
+fclose($stream);
+var_dump($depthWarnings > 0, count(stream_last_errors()));
+?>
+--EXPECT--
+handler: 2 error(s)
+int(16384)
+bool(true)
+int(2)
diff --git a/main/streams/php_stream_errors.h b/main/streams/php_stream_errors.h
index 304ca76a64c..c49f518ea4d 100644
--- a/main/streams/php_stream_errors.h
+++ b/main/streams/php_stream_errors.h
@@ -85,6 +85,7 @@ typedef struct {
php_stream_error_operation *current_operation;
uint32_t operation_depth;
uint32_t operation_floor;
+ uint32_t refused_operations;
php_stream_stored_error *stored_errors;
uint32_t stored_count;
php_stream_error_operation operation_pool[PHP_STREAM_ERROR_OPERATION_POOL_SIZE];
diff --git a/main/streams/stream_errors.c b/main/streams/stream_errors.c
index 204fd4be4a5..e16d3425a4d 100644
--- a/main/streams/stream_errors.c
+++ b/main/streams/stream_errors.c
@@ -232,6 +232,7 @@ PHPAPI void php_stream_error_state_cleanup(void)
php_stream_error_state *state = &FG(stream_error_state);
state->operation_floor = 0;
+ state->refused_operations = 0;
while (state->current_operation) {
php_stream_error_operation *op = state->current_operation;
state->operation_depth--;
@@ -301,6 +302,7 @@ PHPAPI php_stream_error_operation *php_stream_error_operation_begin(void)
php_error_docref(NULL, E_WARNING,
"Stream error operation depth exceeded (%"PRIu32"), possible infinite recursion",
state->operation_depth);
+ state->refused_operations++;
return NULL;
}
@@ -337,7 +339,10 @@ static void php_stream_error_add(zend_enum_StreamErrorCode code, const char *wra
zend_string *message, const char *docref, int severity, bool terminating)
{
php_stream_error_operation *op = FG(stream_error_state).current_operation;
- ZEND_ASSERT(op != NULL);
+ if (!op) {
+ zend_string_release(message);
+ return;
+ }
php_stream_error_entry *entry = emalloc(sizeof(php_stream_error_entry));
entry->message = message;
@@ -446,6 +451,11 @@ PHPAPI void php_stream_error_operation_end(const php_stream_context *context)
php_stream_error_state *state = &FG(stream_error_state);
php_stream_error_operation *op = state->current_operation;
+ if (state->refused_operations > 0) {
+ state->refused_operations--;
+ return;
+ }
+
if (!op) {
return;
}
@@ -552,6 +562,11 @@ PHPAPI void php_stream_error_operation_end_for_stream(const php_stream *stream)
php_stream_error_state *state = &FG(stream_error_state);
php_stream_error_operation *op = state->current_operation;
+ if (state->refused_operations > 0) {
+ state->refused_operations--;
+ return;
+ }
+
if (!op) {
return;
}
@@ -574,6 +589,11 @@ PHPAPI void php_stream_error_operation_abort(void)
php_stream_error_state *state = &FG(stream_error_state);
php_stream_error_operation *op = state->current_operation;
+ if (state->refused_operations > 0) {
+ state->refused_operations--;
+ return;
+ }
+
if (!op) {
return;
}