Skip to content

Commit d74085b

Browse files
Merge branch 'PHP-8.4' into PHP-8.5
* PHP-8.4: Fix GH-20370: forbid user stream filters to violate typed property constraints (#20373)
2 parents 6a0da6d + 175548e commit d74085b

File tree

6 files changed

+205
-9
lines changed

6 files changed

+205
-9
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ PHP NEWS
7474
via deep structures). (ndossche)
7575
. Fixed bug GH-20584 (Information Leak of Memory). (ndossche)
7676

77+
- Streams:
78+
. Fixed bug GH-20370 (User stream filters could violate typed property
79+
constraints). (alexandre-daubois)
80+
7781
- XML:
7882
. Fixed bug GH-20439 (xml_set_default_handler() does not properly handle
7983
special characters in attributes when passing data to callback). (ndossche)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
GH-20370 (User filters should respect typed properties)
3+
--FILE--
4+
<?php
5+
6+
class pass_filter
7+
{
8+
public $filtername;
9+
public $params;
10+
public int $stream = 1;
11+
12+
function filter($in, $out, &$consumed, $closing): int
13+
{
14+
while ($bucket = stream_bucket_make_writeable($in)) {
15+
$consumed += $bucket->datalen;
16+
stream_bucket_append($out, $bucket);
17+
}
18+
return PSFS_PASS_ON;
19+
}
20+
}
21+
22+
stream_filter_register("pass", "pass_filter");
23+
$fp = fopen("php://memory", "w");
24+
stream_filter_append($fp, "pass");
25+
26+
try {
27+
fwrite($fp, "data");
28+
} catch (TypeError $e) {
29+
echo $e::class, ": ", $e->getMessage(), "\n";
30+
}
31+
32+
try {
33+
fclose($fp);
34+
} catch (TypeError $e) {
35+
echo $e::class, ": ", $e->getMessage(), "\n";
36+
}
37+
38+
unset($fp); // prevent cleanup at shutdown
39+
40+
?>
41+
--EXPECTF--
42+
Warning: fwrite(): Unprocessed filter buckets remaining on input brigade in %s on line %d
43+
TypeError: Cannot assign resource to property pass_filter::$stream of type int
44+
TypeError: Cannot assign resource to property pass_filter::$stream of type int
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
--TEST--
2+
GH-20370 (User filters should update dynamic stream property if it exists)
3+
--FILE--
4+
<?php
5+
6+
#[\AllowDynamicProperties]
7+
class pass_filter
8+
{
9+
public $filtername;
10+
public $params;
11+
12+
function onCreate(): bool
13+
{
14+
$this->stream = null;
15+
return true;
16+
}
17+
18+
function filter($in, $out, &$consumed, $closing): int
19+
{
20+
while ($bucket = stream_bucket_make_writeable($in)) {
21+
$consumed += $bucket->datalen;
22+
stream_bucket_append($out, $bucket);
23+
}
24+
var_dump(property_exists($this, 'stream'));
25+
if (is_resource($this->stream)) {
26+
var_dump(get_resource_type($this->stream));
27+
}
28+
return PSFS_PASS_ON;
29+
}
30+
}
31+
32+
stream_filter_register("pass", "pass_filter");
33+
$fp = fopen("php://memory", "w");
34+
stream_filter_append($fp, "pass");
35+
36+
fwrite($fp, "data");
37+
rewind($fp);
38+
echo fread($fp, 1024) . "\n";
39+
40+
?>
41+
--EXPECTF--
42+
bool(true)
43+
string(6) "stream"
44+
bool(true)
45+
string(6) "stream"
46+
bool(true)
47+
string(6) "stream"
48+
bool(true)
49+
string(6) "stream"
50+
data
51+
bool(true)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
GH-20370 (User filters should not create stream property if not declared)
3+
--FILE--
4+
<?php
5+
6+
class pass_filter
7+
{
8+
public $filtername;
9+
public $params;
10+
11+
function filter($in, $out, &$consumed, $closing): int
12+
{
13+
while ($bucket = stream_bucket_make_writeable($in)) {
14+
$consumed += $bucket->datalen;
15+
stream_bucket_append($out, $bucket);
16+
}
17+
18+
var_dump(property_exists($this, 'stream'));
19+
return PSFS_PASS_ON;
20+
}
21+
}
22+
23+
stream_filter_register("pass", "pass_filter");
24+
$fp = fopen("php://memory", "w");
25+
stream_filter_append($fp, "pass");
26+
fwrite($fp, "data");
27+
rewind($fp);
28+
echo fread($fp, 1024) . "\n";
29+
30+
?>
31+
--EXPECT--
32+
bool(false)
33+
bool(false)
34+
bool(false)
35+
bool(false)
36+
data
37+
bool(false)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
GH-20370 (User filters should handle private stream property correctly)
3+
--FILE--
4+
<?php
5+
6+
class pass_filter
7+
{
8+
public $filtername;
9+
public $params;
10+
private $stream;
11+
12+
function filter($in, $out, &$consumed, $closing): int
13+
{
14+
while ($bucket = stream_bucket_make_writeable($in)) {
15+
$consumed += $bucket->datalen;
16+
stream_bucket_append($out, $bucket);
17+
}
18+
return PSFS_PASS_ON;
19+
}
20+
21+
function onClose()
22+
{
23+
var_dump($this->stream); // should be null
24+
}
25+
}
26+
27+
stream_filter_register("pass", "pass_filter");
28+
$fp = fopen("php://memory", "w");
29+
stream_filter_append($fp, "pass", STREAM_FILTER_WRITE);
30+
31+
fwrite($fp, "data");
32+
rewind($fp);
33+
echo fread($fp, 1024) . "\n";
34+
35+
?>
36+
--EXPECT--
37+
data
38+
NULL

ext/standard/user_filters.c

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,31 @@ static php_stream_filter_status_t userfilter_filter(
148148
uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE;
149149
stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
150150

151-
zval *stream_prop = zend_hash_str_find_ind(Z_OBJPROP_P(obj), "stream", sizeof("stream")-1);
152-
if (stream_prop) {
153-
/* Give the userfilter class a hook back to the stream */
154-
zval_ptr_dtor(stream_prop);
155-
php_stream_to_zval(stream, stream_prop);
156-
Z_ADDREF_P(stream_prop);
151+
/* Give the userfilter class a hook back to the stream */
152+
zend_class_entry *old_scope = EG(fake_scope);
153+
EG(fake_scope) = Z_OBJCE_P(obj);
154+
155+
zend_string *stream_name = ZSTR_INIT_LITERAL("stream", 0);
156+
bool stream_property_exists = Z_OBJ_HT_P(obj)->has_property(Z_OBJ_P(obj), stream_name, ZEND_PROPERTY_EXISTS, NULL);
157+
if (stream_property_exists) {
158+
zval stream_zval;
159+
php_stream_to_zval(stream, &stream_zval);
160+
zend_update_property_ex(Z_OBJCE_P(obj), Z_OBJ_P(obj), stream_name, &stream_zval);
161+
/* If property update threw an exception, skip filter execution */
162+
if (EG(exception)) {
163+
EG(fake_scope) = old_scope;
164+
if (buckets_in->head) {
165+
php_error_docref(NULL, E_WARNING, "Unprocessed filter buckets remaining on input brigade");
166+
}
167+
zend_string_release(stream_name);
168+
stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
169+
stream->flags |= orig_no_fclose;
170+
return PSFS_ERR_FATAL;
171+
}
157172
}
158173

174+
EG(fake_scope) = old_scope;
175+
159176
ZVAL_STRINGL(&func_name, "filter", sizeof("filter")-1);
160177

161178
/* Setup calling arguments */
@@ -196,11 +213,16 @@ static php_stream_filter_status_t userfilter_filter(
196213

197214
/* filter resources are cleaned up by the stream destructor,
198215
* keeping a reference to the stream resource here would prevent it
199-
* from being destroyed properly */
200-
if (stream_prop) {
201-
convert_to_null(stream_prop);
216+
* from being destroyed properly.
217+
* Since the property accepted a resource assignment above, it must have
218+
* no type hint or be typed as mixed, so we can safely assign null.
219+
*/
220+
if (stream_property_exists) {
221+
zend_update_property_null(Z_OBJCE_P(obj), Z_OBJ_P(obj), ZSTR_VAL(stream_name), ZSTR_LEN(stream_name));
202222
}
203223

224+
zend_string_release(stream_name);
225+
204226
zval_ptr_dtor(&args[3]);
205227
zval_ptr_dtor(&args[2]);
206228
zval_ptr_dtor(&args[1]);

0 commit comments

Comments
 (0)