From 1421ebc476dd2e8fcf6b12618f42f72a0b87cc2f Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 26 Oct 2026 11:28:53 -0400 Subject: [PATCH] streams: Keep user wrappers alive across URL operations A user wrapper's constructor or destructor can unregister the wrapper while unlink(), rename(), mkdir(), rmdir(), touch(), chmod() or stat() is using it, which frees the wrapper, and the "is not implemented!" warning then reads the freed wrapper and its class. Hold a reference to the wrapper resource for the whole operation, as user_wrapper_opener() and user_wrapper_opendir() already do. --- NEWS | 3 + .../user_wrapper_unregister_url_ops.phpt | 93 +++++++++++++++++++ main/streams/userspace.c | 20 ++++ 3 files changed, 116 insertions(+) create mode 100644 Zend/tests/streams/user_wrapper_unregister_url_ops.phpt diff --git a/NEWS b/NEWS index cd9ddd3994cc..5e0f79f96fb5 100644 --- a/NEWS +++ b/NEWS @@ -143,6 +143,9 @@ PHP NEWS - Streams: . Applied the so_linger, so_rcvbuf and so_sndbuf context options to unix sockets. (David Carlier) + . Fixed a use-after-free when a user stream wrapper unregisters itself from + its constructor or destructor during unlink(), rename(), mkdir(), rmdir(), + touch(), chmod() or stat(). (Ilia Alshanetsky) - Standard: . Fixed bug #60110 (fclose(), file_put_contents(), copy() do not return false diff --git a/Zend/tests/streams/user_wrapper_unregister_url_ops.phpt b/Zend/tests/streams/user_wrapper_unregister_url_ops.phpt new file mode 100644 index 000000000000..9605fa58aa1a --- /dev/null +++ b/Zend/tests/streams/user_wrapper_unregister_url_ops.phpt @@ -0,0 +1,93 @@ +--TEST-- +User stream wrapper unregistered by its own constructor or destructor during a URL operation +--FILE-- + fn() => unlink('foo://a'), + 'rename' => fn() => rename('foo://a', 'foo://b'), + 'mkdir' => fn() => mkdir('foo://a'), + 'rmdir' => fn() => rmdir('foo://a'), + 'touch' => fn() => touch('foo://a'), + 'chmod' => fn() => chmod('foo://a', 0644), + 'stat' => fn() => stat('foo://a'), +]; + +foreach (['Ctor', 'Dtor'] as $class) { + foreach ($ops as $name => $op) { + stream_wrapper_register('foo', $class); + echo "$class $name: "; + var_dump($op()); + var_dump(in_array('foo', stream_get_wrappers())); + } +} +?> +--EXPECTF-- +Ctor unlink: +Warning: unlink(): Ctor::unlink is not implemented! in %s on line %d +bool(false) +bool(false) +Ctor rename: +Warning: rename(): Ctor::rename is not implemented! in %s on line %d +bool(false) +bool(false) +Ctor mkdir: +Warning: mkdir(): Ctor::mkdir is not implemented! in %s on line %d +bool(false) +bool(false) +Ctor rmdir: +Warning: rmdir(): Ctor::rmdir is not implemented! in %s on line %d +bool(false) +bool(false) +Ctor touch: +Warning: touch(): Ctor::stream_metadata is not implemented! in %s on line %d +bool(false) +bool(false) +Ctor chmod: +Warning: chmod(): Ctor::stream_metadata is not implemented! in %s on line %d +bool(false) +bool(false) +Ctor stat: +Warning: stat(): Ctor::url_stat is not implemented! in %s on line %d + +Warning: stat(): stat failed for foo://a in %s on line %d +bool(false) +bool(false) +Dtor unlink: +Warning: unlink(): Dtor::unlink is not implemented! in %s on line %d +bool(false) +bool(false) +Dtor rename: +Warning: rename(): Dtor::rename is not implemented! in %s on line %d +bool(false) +bool(false) +Dtor mkdir: +Warning: mkdir(): Dtor::mkdir is not implemented! in %s on line %d +bool(false) +bool(false) +Dtor rmdir: +Warning: rmdir(): Dtor::rmdir is not implemented! in %s on line %d +bool(false) +bool(false) +Dtor touch: +Warning: touch(): Dtor::stream_metadata is not implemented! in %s on line %d +bool(false) +bool(false) +Dtor chmod: +Warning: chmod(): Dtor::stream_metadata is not implemented! in %s on line %d +bool(false) +bool(false) +Dtor stat: +Warning: stat(): Dtor::url_stat is not implemented! in %s on line %d + +Warning: stat(): stat failed for foo://a in %s on line %d +bool(false) +bool(false) diff --git a/main/streams/userspace.c b/main/streams/userspace.c index 8c4b43f6ce83..97885d0e3c0f 100644 --- a/main/streams/userspace.c +++ b/main/streams/userspace.c @@ -1096,8 +1096,10 @@ static int user_wrapper_unlink(php_stream_wrapper *wrapper, const char *url, int int ret = 0; /* create an instance of our class */ + GC_ADDREF(uwrap->resource); zend_object *object = user_stream_create_object(uwrap, context); if (object == NULL) { + zend_list_delete(uwrap->resource); return ret; } @@ -1121,6 +1123,7 @@ static int user_wrapper_unlink(php_stream_wrapper *wrapper, const char *url, int zval_ptr_dtor(&zretval); + zend_list_delete(uwrap->resource); return ret; } @@ -1133,8 +1136,10 @@ static int user_wrapper_rename(php_stream_wrapper *wrapper, const char *url_from int ret = 0; /* create an instance of our class */ + GC_ADDREF(uwrap->resource); zend_object *object = user_stream_create_object(uwrap, context); if (object == NULL) { + zend_list_delete(uwrap->resource); return ret; } @@ -1159,6 +1164,7 @@ static int user_wrapper_rename(php_stream_wrapper *wrapper, const char *url_from zval_ptr_dtor(&zretval); + zend_list_delete(uwrap->resource); return ret; } @@ -1171,8 +1177,10 @@ static int user_wrapper_mkdir(php_stream_wrapper *wrapper, const char *url, int int ret = 0; /* create an instance of our class */ + GC_ADDREF(uwrap->resource); zend_object *object = user_stream_create_object(uwrap, context); if (object == NULL) { + zend_list_delete(uwrap->resource); return ret; } @@ -1197,6 +1205,7 @@ static int user_wrapper_mkdir(php_stream_wrapper *wrapper, const char *url, int zval_ptr_dtor(&zretval); + zend_list_delete(uwrap->resource); return ret; } @@ -1209,8 +1218,10 @@ static int user_wrapper_rmdir(php_stream_wrapper *wrapper, const char *url, int ret = 0; /* create an instance of our class */ + GC_ADDREF(uwrap->resource); zend_object *object = user_stream_create_object(uwrap, context); if (object == NULL) { + zend_list_delete(uwrap->resource); return ret; } @@ -1234,6 +1245,7 @@ static int user_wrapper_rmdir(php_stream_wrapper *wrapper, const char *url, zval_ptr_dtor(&zretval); + zend_list_delete(uwrap->resource); return ret; } @@ -1271,9 +1283,11 @@ static int user_wrapper_metadata(php_stream_wrapper *wrapper, const char *url, i } /* create an instance of our class */ + GC_ADDREF(uwrap->resource); zend_object *object = user_stream_create_object(uwrap, context); if (object == NULL) { zval_ptr_dtor(&args[2]); + zend_list_delete(uwrap->resource); return ret; } @@ -1298,6 +1312,7 @@ static int user_wrapper_metadata(php_stream_wrapper *wrapper, const char *url, i zval_ptr_dtor(&zretval); + zend_list_delete(uwrap->resource); return ret; } @@ -1311,8 +1326,10 @@ static int user_wrapper_stat_url(php_stream_wrapper *wrapper, const char *url, i int ret = -1; /* create an instance of our class */ + GC_ADDREF(uwrap->resource); zend_object *object = user_stream_create_object(uwrap, context); if (object == NULL) { + zend_list_delete(uwrap->resource); return -1; } @@ -1329,9 +1346,11 @@ static int user_wrapper_stat_url(php_stream_wrapper *wrapper, const char *url, i if (UNEXPECTED(call_result == FAILURE)) { php_stream_wrapper_warn(wrapper, context, REPORT_ERRORS, NotImplemented, "%s::" USERSTREAM_STATURL " is not implemented!", ZSTR_VAL(uwrap->ce->name)); + zend_list_delete(uwrap->resource); return -1; } if (UNEXPECTED(Z_ISUNDEF(zretval))) { + zend_list_delete(uwrap->resource); return -1; } if (EXPECTED(Z_TYPE(zretval) == IS_ARRAY)) { @@ -1342,6 +1361,7 @@ static int user_wrapper_stat_url(php_stream_wrapper *wrapper, const char *url, i zval_ptr_dtor(&zretval); + zend_list_delete(uwrap->resource); return ret; }