Skip to content

Commit 4f7928f

Browse files
committed
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.
1 parent e377fd3 commit 4f7928f

3 files changed

Lines changed: 94 additions & 1 deletion

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
--TEST--
2+
Stream errors: operations refused by the depth limit keep the stack consistent
3+
--INI--
4+
zend.max_allowed_stack_size=-1
5+
--FILE--
6+
<?php
7+
class RecursiveStream
8+
{
9+
public $context;
10+
public static int $depth = 0;
11+
private int $reads = 0;
12+
13+
public function stream_open($path, $mode, $options, &$openedPath): bool
14+
{
15+
return true;
16+
}
17+
18+
public function stream_read(int $count): string
19+
{
20+
$outermost = self::$depth === 0;
21+
if (++self::$depth < 1010) {
22+
$f = fopen('recursive-stream://x', 'r', false, $GLOBALS['ctx']);
23+
fread($f, 1);
24+
fclose($f);
25+
}
26+
self::$depth--;
27+
if (!$outermost) {
28+
return 'x';
29+
}
30+
return ++$this->reads <= 2 ? str_repeat('A', $count + 1) : '';
31+
}
32+
33+
public function stream_eof(): bool
34+
{
35+
return self::$depth > 0 || $this->reads >= 3;
36+
}
37+
38+
public function stream_stat(): array
39+
{
40+
return [];
41+
}
42+
}
43+
44+
stream_wrapper_register('recursive-stream', RecursiveStream::class);
45+
46+
$depthWarnings = 0;
47+
set_error_handler(static function (int $severity, string $message) use (&$depthWarnings): bool {
48+
if (str_contains($message, 'depth exceeded')) {
49+
$depthWarnings++;
50+
return true;
51+
}
52+
return false;
53+
});
54+
55+
$ctx = stream_context_create(['stream' => [
56+
'error_mode' => StreamErrorMode::Silent,
57+
'error_store' => StreamErrorStore::All,
58+
'error_handler' => static function (array $errors): void {
59+
echo "handler: " . count($errors) . " error(s)\n";
60+
},
61+
]]);
62+
63+
$stream = fopen('recursive-stream://x', 'r', false, $ctx);
64+
var_dump(strlen(stream_get_contents($stream)));
65+
fclose($stream);
66+
var_dump($depthWarnings > 0, count(stream_last_errors()));
67+
?>
68+
--EXPECT--
69+
handler: 2 error(s)
70+
int(16384)
71+
bool(true)
72+
int(2)

main/streams/php_stream_errors.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ typedef struct {
8585
php_stream_error_operation *current_operation;
8686
uint32_t operation_depth;
8787
uint32_t operation_floor;
88+
uint32_t refused_operations;
8889
php_stream_stored_error *stored_errors;
8990
uint32_t stored_count;
9091
php_stream_error_operation operation_pool[PHP_STREAM_ERROR_OPERATION_POOL_SIZE];

main/streams/stream_errors.c

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ PHPAPI void php_stream_error_state_cleanup(void)
232232
php_stream_error_state *state = &FG(stream_error_state);
233233

234234
state->operation_floor = 0;
235+
state->refused_operations = 0;
235236
while (state->current_operation) {
236237
php_stream_error_operation *op = state->current_operation;
237238
state->operation_depth--;
@@ -301,6 +302,7 @@ PHPAPI php_stream_error_operation *php_stream_error_operation_begin(void)
301302
php_error_docref(NULL, E_WARNING,
302303
"Stream error operation depth exceeded (%"PRIu32"), possible infinite recursion",
303304
state->operation_depth);
305+
state->refused_operations++;
304306
return NULL;
305307
}
306308

@@ -337,7 +339,10 @@ static void php_stream_error_add(zend_enum_StreamErrorCode code, const char *wra
337339
zend_string *message, const char *docref, int severity, bool terminating)
338340
{
339341
php_stream_error_operation *op = FG(stream_error_state).current_operation;
340-
ZEND_ASSERT(op != NULL);
342+
if (!op) {
343+
zend_string_release(message);
344+
return;
345+
}
341346

342347
php_stream_error_entry *entry = emalloc(sizeof(php_stream_error_entry));
343348
entry->message = message;
@@ -446,6 +451,11 @@ PHPAPI void php_stream_error_operation_end(const php_stream_context *context)
446451
php_stream_error_state *state = &FG(stream_error_state);
447452
php_stream_error_operation *op = state->current_operation;
448453

454+
if (state->refused_operations > 0) {
455+
state->refused_operations--;
456+
return;
457+
}
458+
449459
if (!op) {
450460
return;
451461
}
@@ -552,6 +562,11 @@ PHPAPI void php_stream_error_operation_end_for_stream(const php_stream *stream)
552562
php_stream_error_state *state = &FG(stream_error_state);
553563
php_stream_error_operation *op = state->current_operation;
554564

565+
if (state->refused_operations > 0) {
566+
state->refused_operations--;
567+
return;
568+
}
569+
555570
if (!op) {
556571
return;
557572
}
@@ -574,6 +589,11 @@ PHPAPI void php_stream_error_operation_abort(void)
574589
php_stream_error_state *state = &FG(stream_error_state);
575590
php_stream_error_operation *op = state->current_operation;
576591

592+
if (state->refused_operations > 0) {
593+
state->refused_operations--;
594+
return;
595+
}
596+
577597
if (!op) {
578598
return;
579599
}

0 commit comments

Comments
 (0)