From efca8cb682d002d6e1ac847093ebf6bc5078dc2f Mon Sep 17 00:00:00 2001 From: Shivam Mathur Date: Thu, 30 Jan 2025 23:14:59 +0530 Subject: [PATCH 1/8] ci: add workflow to trigger windows builds (#17634) --- .github/workflows/windows-builds.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/windows-builds.yml diff --git a/.github/workflows/windows-builds.yml b/.github/workflows/windows-builds.yml new file mode 100644 index 0000000000000..6bb4cd897164d --- /dev/null +++ b/.github/workflows/windows-builds.yml @@ -0,0 +1,23 @@ +name: Windows builds +run-name: Windows builds for ${{ inputs.tag || github.ref_name }} +on: + push: + tags: + - 'php-*' + workflow_dispatch: + inputs: + tag: + description: 'Tag version' + required: true + +jobs: + publish: + runs-on: ubuntu-latest + name: Build + steps: + - name: Build + env: + GITHUB_TOKEN: ${{ secrets.WINDOWS_BUILDS_TOKEN }} + run: | + TAG="${{ inputs.tag || github.ref_name }}" + gh workflow run php.yml -R php/php-windows-builder -f php-version="${TAG#php-}" From 6e84c41d051174d71cf1ebc6533d7c8072b5642d Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 26 Jan 2025 19:26:36 +0100 Subject: [PATCH 2/8] Fix GH-12856: ReflectionClass::getStaticPropertyValue() returns UNDEF zval for uninitialized typed properties Closes GH-17590. --- NEWS | 2 ++ ext/reflection/php_reflection.c | 11 ++++++++--- ext/reflection/tests/gh12856.phpt | 27 +++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 ext/reflection/tests/gh12856.phpt diff --git a/NEWS b/NEWS index 250d7017ee6d6..752b49cdc15be 100644 --- a/NEWS +++ b/NEWS @@ -81,6 +81,8 @@ PHP NEWS - Reflection: . Added ReflectionConstant::getExtension() and ::getExtensionName(). (DanielEScherzer) + . Fixed bug GH-12856 (ReflectionClass::getStaticPropertyValue() returns UNDEF + zval for uninitialized typed properties). (nielsdos) - Session: . session_start() throws a ValueError on option argument if not a hashmap diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index f5e463699b1b5..724de0a491240 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -4128,7 +4128,7 @@ ZEND_METHOD(ReflectionClass, getStaticPropertyValue) prop = zend_std_get_static_property(ce, name, BP_VAR_IS); EG(fake_scope) = old_scope; - if (prop) { + if (prop && !Z_ISUNDEF_P(prop)) { RETURN_COPY_DEREF(prop); } @@ -4136,8 +4136,13 @@ ZEND_METHOD(ReflectionClass, getStaticPropertyValue) RETURN_COPY(def_value); } - zend_throw_exception_ex(reflection_exception_ptr, 0, - "Property %s::$%s does not exist", ZSTR_VAL(ce->name), ZSTR_VAL(name)); + if (prop) { + zend_throw_error(NULL, + "Typed property %s::$%s must not be accessed before initialization", ZSTR_VAL(ce->name), ZSTR_VAL(name)); + } else { + zend_throw_exception_ex(reflection_exception_ptr, 0, + "Property %s::$%s does not exist", ZSTR_VAL(ce->name), ZSTR_VAL(name)); + } } /* }}} */ diff --git a/ext/reflection/tests/gh12856.phpt b/ext/reflection/tests/gh12856.phpt new file mode 100644 index 0000000000000..519a222ab0ed7 --- /dev/null +++ b/ext/reflection/tests/gh12856.phpt @@ -0,0 +1,27 @@ +--TEST-- +GH-12856 (ReflectionClass::getStaticPropertyValue() returns UNDEF zval for uninitialized typed properties) +--FILE-- +getStaticPropertyValue('untyped')); +try { + var_dump($rc->getStaticPropertyValue('typed1')); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($rc->getStaticPropertyValue('typed1', 1)); +var_dump($rc->getStaticPropertyValue('typed2')); + +?> +--EXPECT-- +NULL +Typed property Bug::$typed1 must not be accessed before initialization +int(1) +int(3) From 5447473785b9410bf895c36a6ace065bdab55d7a Mon Sep 17 00:00:00 2001 From: ndossche Date: Thu, 30 Jan 2025 10:29:46 +0100 Subject: [PATCH 3/8] Partially fix GH-17387 The length of the string should be set to the truncated length (that was used to duplicate the input anyway). --- NEWS | 3 +++ sapi/phpdbg/phpdbg_lexer.l | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index 036d611344452..7c51ecc121c85 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,9 @@ PHP NEWS . Fixed bug GH-17623 (Broken stack overflow detection for variable compilation). (ilutov) +- PHPDBG: + . Partially fixed bug GH-17387 (Trivial crash in phpdbg lexer). (nielsdos) + 13 Feb 2025, PHP 8.3.17 - Core: diff --git a/sapi/phpdbg/phpdbg_lexer.l b/sapi/phpdbg/phpdbg_lexer.l index 6245262a00598..60d995526ea27 100644 --- a/sapi/phpdbg/phpdbg_lexer.l +++ b/sapi/phpdbg/phpdbg_lexer.l @@ -160,8 +160,9 @@ INPUT ("\\"[#"']|["]("\\\\"|"\\"["]|[^\n\000"])*["]|[']("\\"[']|"\\\\"|[^\ {GENERIC_ID} { phpdbg_init_param(yylval, STR_PARAM); - yylval->str = estrndup(yytext, yyleng - unescape_string(yytext)); - yylval->len = yyleng; + size_t len = yyleng - unescape_string(yytext); + yylval->str = estrndup(yytext, len); + yylval->len = len; return T_ID; } From 62bbfdebaa0458eee31bb015b507f36653fcdf5c Mon Sep 17 00:00:00 2001 From: ndossche Date: Thu, 30 Jan 2025 10:30:25 +0100 Subject: [PATCH 4/8] Fix memory leak in phpdbg calling registered function Closes GH-17635. --- NEWS | 1 + sapi/phpdbg/phpdbg_prompt.c | 3 +++ sapi/phpdbg/tests/register_function_leak.phpt | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 sapi/phpdbg/tests/register_function_leak.phpt diff --git a/NEWS b/NEWS index 7c51ecc121c85..ca459617aafb5 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,7 @@ PHP NEWS - PHPDBG: . Partially fixed bug GH-17387 (Trivial crash in phpdbg lexer). (nielsdos) + . Fix memory leak in phpdbg calling registered function. (nielsdos) 13 Feb 2025, PHP 8.3.17 diff --git a/sapi/phpdbg/phpdbg_prompt.c b/sapi/phpdbg/phpdbg_prompt.c index 5276d62ee295e..e43bf647c7611 100644 --- a/sapi/phpdbg/phpdbg_prompt.c +++ b/sapi/phpdbg/phpdbg_prompt.c @@ -189,6 +189,9 @@ static inline int phpdbg_call_register(phpdbg_param_t *stack) /* {{{ */ zval_ptr_dtor_str(&fci.function_name); efree(lc_name); + if (fci.named_params) { + zend_array_destroy(fci.named_params); + } return SUCCESS; } diff --git a/sapi/phpdbg/tests/register_function_leak.phpt b/sapi/phpdbg/tests/register_function_leak.phpt new file mode 100644 index 0000000000000..b5416ea95bcc8 --- /dev/null +++ b/sapi/phpdbg/tests/register_function_leak.phpt @@ -0,0 +1,24 @@ +--TEST-- +registering a function and calling it leaks arguments memory +--FILE-- + +--PHPDBG-- +register var_dump +var_dump "a" "b" +register flush +flush +r +q +--EXPECTF-- +[Successful compilation of %s] +prompt> [Registered var_dump] +prompt> string(1) "a" +string(1) "b" + +prompt> [Registered flush] +prompt> +prompt> Done +[Script ended normally] +prompt> From 09791ed1d1200c58c82584671054cd2e1894a3ac Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 30 Jan 2025 18:34:03 +0000 Subject: [PATCH 5/8] ext/pdo: Convert database_object_handle zval to zend_object* (#17629) This saves 8 bytes on the PDO statement struct. We change the PGSQL PDO driver API to take a zend_object* instead of a zval* at the same time. --- ext/pdo/pdo_dbh.c | 16 +++++++++------- ext/pdo/pdo_stmt.c | 9 +++++---- ext/pdo/php_pdo_driver.h | 2 +- ext/pdo_pgsql/pgsql_driver.c | 10 +++++----- ext/pdo_pgsql/pgsql_statement.c | 2 +- ext/pdo_pgsql/php_pdo_pgsql_int.h | 2 +- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/ext/pdo/pdo_dbh.c b/ext/pdo/pdo_dbh.c index 377615f2fe058..10af61fa2b08f 100644 --- a/ext/pdo/pdo_dbh.c +++ b/ext/pdo/pdo_dbh.c @@ -67,9 +67,9 @@ void pdo_throw_exception(unsigned int driver_errcode, char *driver_errmsg, pdo_e PDO_API bool php_pdo_stmt_valid_db_obj_handle(const pdo_stmt_t *stmt) { - return !Z_ISUNDEF(stmt->database_object_handle) - && IS_OBJ_VALID(EG(objects_store).object_buckets[Z_OBJ_HANDLE(stmt->database_object_handle)]) - && !(OBJ_FLAGS(Z_OBJ(stmt->database_object_handle)) & IS_OBJ_FREE_CALLED); + return stmt->database_object_handle != NULL + && IS_OBJ_VALID(EG(objects_store).object_buckets[stmt->database_object_handle->handle]) + && !(OBJ_FLAGS(stmt->database_object_handle) & IS_OBJ_FREE_CALLED); } void pdo_raise_impl_error(pdo_dbh_t *dbh, pdo_stmt_t *stmt, pdo_error_type sqlstate, const char *supp) /* {{{ */ @@ -657,7 +657,8 @@ PHP_METHOD(PDO, prepare) stmt->default_fetch_type = dbh->default_fetch_type; stmt->dbh = dbh; /* give it a reference to me */ - ZVAL_OBJ_COPY(&stmt->database_object_handle, &dbh_obj->std); + GC_ADDREF(&dbh_obj->std); + stmt->database_object_handle = &dbh_obj->std; /* we haven't created a lazy object yet */ ZVAL_UNDEF(&stmt->lazy_object_ref); @@ -1222,7 +1223,8 @@ PHP_METHOD(PDO, query) stmt->default_fetch_type = dbh->default_fetch_type; stmt->dbh = dbh; /* give it a reference to me */ - ZVAL_OBJ_COPY(&stmt->database_object_handle, &dbh_obj->std); + GC_ADDREF(&dbh_obj->std); + stmt->database_object_handle = &dbh_obj->std; /* we haven't created a lazy object yet */ ZVAL_UNDEF(&stmt->lazy_object_ref); @@ -1252,8 +1254,8 @@ PHP_METHOD(PDO, query) /* something broke */ dbh->query_stmt = stmt; ZVAL_OBJ(&dbh->query_stmt_zval, Z_OBJ_P(return_value)); - Z_DELREF(stmt->database_object_handle); - ZVAL_UNDEF(&stmt->database_object_handle); + GC_DELREF(stmt->database_object_handle); + stmt->database_object_handle = NULL; PDO_HANDLE_STMT_ERR(); } else { PDO_HANDLE_DBH_ERR(); diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c index 88b7af51b2d16..cb9fdf957af76 100644 --- a/ext/pdo/pdo_stmt.c +++ b/ext/pdo/pdo_stmt.c @@ -2021,7 +2021,7 @@ static zend_function *dbstmt_method_get(zend_object **object_pp, zend_string *me /* not a pre-defined method, nor a user-defined method; check * the driver specific methods */ if (!stmt->dbh->cls_methods[PDO_DBH_DRIVER_METHOD_KIND_STMT]) { - if (!pdo_hash_methods(Z_PDO_OBJECT_P(&stmt->database_object_handle), + if (!pdo_hash_methods(php_pdo_dbh_fetch_object(stmt->database_object_handle), PDO_DBH_DRIVER_METHOD_KIND_STMT) || !stmt->dbh->cls_methods[PDO_DBH_DRIVER_METHOD_KIND_STMT]) { goto out; @@ -2048,7 +2048,7 @@ static HashTable *dbstmt_get_gc(zend_object *object, zval **gc_data, int *gc_cou enum pdo_fetch_type default_fetch_mode = stmt->default_fetch_type & ~PDO_FETCH_FLAGS; zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create(); - zend_get_gc_buffer_add_zval(gc_buffer, &stmt->database_object_handle); + zend_get_gc_buffer_add_obj(gc_buffer, stmt->database_object_handle); if (default_fetch_mode == PDO_FETCH_INTO) { zend_get_gc_buffer_add_obj(gc_buffer, stmt->fetch.into); } else if (default_fetch_mode == PDO_FETCH_CLASS) { @@ -2107,8 +2107,9 @@ PDO_API void php_pdo_free_statement(pdo_stmt_t *stmt) do_fetch_opt_finish(stmt, 1); - if (!Z_ISUNDEF(stmt->database_object_handle)) { - zval_ptr_dtor(&stmt->database_object_handle); + if (stmt->database_object_handle != NULL) { + OBJ_RELEASE(stmt->database_object_handle); + stmt->database_object_handle = NULL; } zend_object_std_dtor(&stmt->std); } diff --git a/ext/pdo/php_pdo_driver.h b/ext/pdo/php_pdo_driver.h index cb483a6bf6b08..94e7c805bfde8 100644 --- a/ext/pdo/php_pdo_driver.h +++ b/ext/pdo/php_pdo_driver.h @@ -578,7 +578,7 @@ struct _pdo_stmt_t { struct pdo_column_data *columns; /* we want to keep the dbh alive while we live, so we own a reference */ - zval database_object_handle; + zend_object *database_object_handle; pdo_dbh_t *dbh; /* keep track of bound input parameters. Some drivers support diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c index c7944373fa7c7..1a46cafd8f520 100644 --- a/ext/pdo_pgsql/pgsql_driver.c +++ b/ext/pdo_pgsql/pgsql_driver.c @@ -200,13 +200,13 @@ const php_stream_ops pdo_pgsql_lob_stream_ops = { NULL }; -php_stream *pdo_pgsql_create_lob_stream(zval *dbh, int lfd, Oid oid) +php_stream *pdo_pgsql_create_lob_stream(zend_object *dbh, int lfd, Oid oid) { php_stream *stm; struct pdo_pgsql_lob_self *self = ecalloc(1, sizeof(*self)); - pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)(Z_PDO_DBH_P(dbh))->driver_data; + pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)(php_pdo_dbh_fetch_inner(dbh))->driver_data; - ZVAL_COPY_VALUE(&self->dbh, dbh); + ZVAL_OBJ(&self->dbh, dbh); self->lfd = lfd; self->oid = oid; self->conn = H->server; @@ -214,7 +214,7 @@ php_stream *pdo_pgsql_create_lob_stream(zval *dbh, int lfd, Oid oid) stm = php_stream_alloc(&pdo_pgsql_lob_stream_ops, self, 0, "r+b"); if (stm) { - Z_ADDREF_P(dbh); + GC_ADDREF(dbh); zend_hash_index_add_ptr(H->lob_streams, php_stream_get_resource_id(stm), stm->res); return stm; } @@ -1116,7 +1116,7 @@ void pgsqlLOBOpen_internal(INTERNAL_FUNCTION_PARAMETERS) lfd = lo_open(H->server, oid, mode); if (lfd >= 0) { - php_stream *stream = pdo_pgsql_create_lob_stream(ZEND_THIS, lfd, oid); + php_stream *stream = pdo_pgsql_create_lob_stream(Z_OBJ_P(ZEND_THIS), lfd, oid); if (stream) { php_stream_to_zval(stream, return_value); return; diff --git a/ext/pdo_pgsql/pgsql_statement.c b/ext/pdo_pgsql/pgsql_statement.c index 1d5e188cf5430..3485bd8df0024 100644 --- a/ext/pdo_pgsql/pgsql_statement.c +++ b/ext/pdo_pgsql/pgsql_statement.c @@ -661,7 +661,7 @@ static int pgsql_stmt_get_col(pdo_stmt_t *stmt, int colno, zval *result, enum pd /* If column was bound as LOB, return a stream. */ int loid = lo_open(S->H->server, oid, INV_READ); if (loid >= 0) { - php_stream *stream = pdo_pgsql_create_lob_stream(&stmt->database_object_handle, loid, oid); + php_stream *stream = pdo_pgsql_create_lob_stream(stmt->database_object_handle, loid, oid); if (stream) { php_stream_to_zval(stream, result); return 1; diff --git a/ext/pdo_pgsql/php_pdo_pgsql_int.h b/ext/pdo_pgsql/php_pdo_pgsql_int.h index 7dee249cbb110..881b4e7046504 100644 --- a/ext/pdo_pgsql/php_pdo_pgsql_int.h +++ b/ext/pdo_pgsql/php_pdo_pgsql_int.h @@ -112,7 +112,7 @@ enum pdo_pgsql_specific_constants { PGSQL_TRANSACTION_UNKNOWN = PQTRANS_UNKNOWN }; -php_stream *pdo_pgsql_create_lob_stream(zval *pdh, int lfd, Oid oid); +php_stream *pdo_pgsql_create_lob_stream(zend_object *pdh, int lfd, Oid oid); extern const php_stream_ops pdo_pgsql_lob_stream_ops; void pdo_pgsql_cleanup_notice_callback(pdo_pgsql_db_handle *H); From b068c2ff9482c76acf307784285928640eb346da Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 11 Jan 2025 11:26:59 +0100 Subject: [PATCH 6/8] Fix GH-17442: Engine UAF with reference assign and dtor Closes GH-17443. --- NEWS | 1 + UPGRADING.INTERNALS | 6 +++++ Zend/tests/weakrefs/gh17442_1.phpt | 22 ++++++++++++++++++ Zend/tests/weakrefs/gh17442_2.phpt | 35 ++++++++++++++++++++++++++++ Zend/zend_API.c | 3 +-- Zend/zend_API.h | 37 ++++++++++++++---------------- Zend/zend_execute.h | 11 +++++++++ Zend/zend_variables.c | 14 +++++++++++ Zend/zend_variables.h | 1 + 9 files changed, 108 insertions(+), 22 deletions(-) create mode 100644 Zend/tests/weakrefs/gh17442_1.phpt create mode 100644 Zend/tests/weakrefs/gh17442_2.phpt diff --git a/NEWS b/NEWS index 752b49cdc15be..24f4963802b90 100644 --- a/NEWS +++ b/NEWS @@ -17,6 +17,7 @@ PHP NEWS . Implement GH-15680 (Enhance zend_dump_op_array to properly represent non-printable characters in string literals). (nielsdos, WangYihang) . Add support for backtraces for fatal errors. (enorris) + . Fixed bug GH-17442 (Engine UAF with reference assign and dtor). (nielsdos) - Curl: . Added curl_multi_get_handles(). (timwolla) diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 64ed35af67e6a..d7a63e93b3dcd 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -14,6 +14,12 @@ PHP 8.5 INTERNALS UPGRADE NOTES 1. Internal API changes ======================== +- Zend + . Added zend_safe_assign_to_variable_noref() function to safely assign + a value to a non-reference zval. + . Added zval_ptr_safe_dtor() to safely destroy a zval when a destructor + could interfere. + ======================== 2. Build system changes ======================== diff --git a/Zend/tests/weakrefs/gh17442_1.phpt b/Zend/tests/weakrefs/gh17442_1.phpt new file mode 100644 index 0000000000000..fc7f60174ed9e --- /dev/null +++ b/Zend/tests/weakrefs/gh17442_1.phpt @@ -0,0 +1,22 @@ +--TEST-- +GH-17442 (Engine UAF with reference assign and dtor) - untyped +--CREDITS-- +YuanchengJiang +--FILE-- + +--EXPECTF-- +Fatal error: Uncaught Exception: Test in %s:%d +Stack trace: +#0 [internal function]: class@anonymous->__destruct() +#1 %s(%d): headers_sent(NULL, 0) +#2 {main} + thrown in %s on line %d diff --git a/Zend/tests/weakrefs/gh17442_2.phpt b/Zend/tests/weakrefs/gh17442_2.phpt new file mode 100644 index 0000000000000..296a23a651383 --- /dev/null +++ b/Zend/tests/weakrefs/gh17442_2.phpt @@ -0,0 +1,35 @@ +--TEST-- +GH-17442 (Engine UAF with reference assign and dtor) - typed +--CREDITS-- +YuanchengJiang +nielsdos +--FILE-- +obj = new stdClass; + +$map[$test->obj] = new class { + function __destruct() { + global $test; + var_dump($test->obj); + throw new Exception("Test"); + } +}; + +headers_sent($test->obj); +?> +--EXPECTF-- +string(0) "" + +Fatal error: Uncaught Exception: Test in %s:%d +Stack trace: +#0 [internal function]: class@anonymous->__destruct() +#1 %s(%d): headers_sent('') +#2 {main} + thrown in %s on line %d diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 8c239a952f32b..90bb9370e1fdd 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -4666,8 +4666,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_ex(zend_reference *ref, zval *val zval_ptr_dtor(val); return FAILURE; } else { - zval_ptr_dtor(&ref->val); - ZVAL_COPY_VALUE(&ref->val, val); + zend_safe_assign_to_variable_noref(&ref->val, val); return SUCCESS; } } diff --git a/Zend/zend_API.h b/Zend/zend_API.h index d91da91bf299e..6aeffce25d8e5 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -1107,7 +1107,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_NULL(_zv); \ } while (0) @@ -1129,7 +1129,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_FALSE(_zv); \ } while (0) @@ -1151,7 +1151,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_TRUE(_zv); \ } while (0) @@ -1173,7 +1173,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_BOOL(_zv, bval); \ } while (0) @@ -1195,7 +1195,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_LONG(_zv, lval); \ } while (0) @@ -1217,7 +1217,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_DOUBLE(_zv, dval); \ } while (0) @@ -1239,7 +1239,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_EMPTY_STRING(_zv); \ } while (0) @@ -1261,7 +1261,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_STR(_zv, str); \ } while (0) @@ -1283,7 +1283,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_NEW_STR(_zv, str); \ } while (0) @@ -1305,7 +1305,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_STRING(_zv, string); \ } while (0) @@ -1327,7 +1327,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_STRINGL(_zv, string, len); \ } while (0) @@ -1349,7 +1349,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_ARR(_zv, arr); \ } while (0) @@ -1371,7 +1371,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_RES(_zv, res); \ } while (0) @@ -1393,7 +1393,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_COPY_VALUE(_zv, other_zv); \ } while (0) @@ -1415,7 +1415,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_COPY_VALUE(_zv, other_zv); \ } while (0) @@ -1447,7 +1447,7 @@ ZEND_API zend_result zend_try_assign_typed_ref_zval_ex(zend_reference *ref, zval } \ _zv = &ref->val; \ } \ - zval_ptr_dtor(_zv); \ + zval_ptr_safe_dtor(_zv); \ ZVAL_COPY_VALUE(_zv, other_zv); \ } while (0) @@ -1485,10 +1485,7 @@ static zend_always_inline zval *zend_try_array_init_size(zval *zv, uint32_t size } zv = &ref->val; } - zval garbage; - ZVAL_COPY_VALUE(&garbage, zv); - ZVAL_NULL(zv); - zval_ptr_dtor(&garbage); + zval_ptr_safe_dtor(zv); ZVAL_ARR(zv, arr); return zv; } diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index a1fbe049f3f99..1734269186116 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -207,6 +207,17 @@ static zend_always_inline zval* zend_assign_to_variable_ex(zval *variable_ptr, z return variable_ptr; } +static zend_always_inline void zend_safe_assign_to_variable_noref(zval *variable_ptr, zval *value) { + if (Z_REFCOUNTED_P(variable_ptr)) { + ZEND_ASSERT(Z_TYPE_P(variable_ptr) != IS_REFERENCE); + zend_refcounted *ref = Z_COUNTED_P(variable_ptr); + ZVAL_COPY_VALUE(variable_ptr, value); + GC_DTOR_NO_REF(ref); + } else { + ZVAL_COPY_VALUE(variable_ptr, value); + } +} + ZEND_API zend_result ZEND_FASTCALL zval_update_constant(zval *pp); ZEND_API zend_result ZEND_FASTCALL zval_update_constant_ex(zval *pp, zend_class_entry *scope); ZEND_API zend_result ZEND_FASTCALL zval_update_constant_with_ctx(zval *pp, zend_class_entry *scope, zend_ast_evaluate_ctx *ctx); diff --git a/Zend/zend_variables.c b/Zend/zend_variables.c index 27e09d7db22b1..00f10b08f80ab 100644 --- a/Zend/zend_variables.c +++ b/Zend/zend_variables.c @@ -85,6 +85,20 @@ ZEND_API void zval_ptr_dtor(zval *zval_ptr) /* {{{ */ } /* }}} */ +ZEND_API void zval_ptr_safe_dtor(zval *zval_ptr) +{ + if (Z_REFCOUNTED_P(zval_ptr)) { + zend_refcounted *ref = Z_COUNTED_P(zval_ptr); + + if (GC_DELREF(ref) == 0) { + ZVAL_NULL(zval_ptr); + rc_dtor_func(ref); + } else { + gc_check_possible_root(ref); + } + } +} + ZEND_API void zval_internal_ptr_dtor(zval *zval_ptr) /* {{{ */ { if (Z_REFCOUNTED_P(zval_ptr)) { diff --git a/Zend/zend_variables.h b/Zend/zend_variables.h index d504b0f0f5795..1cb745ca1b1dc 100644 --- a/Zend/zend_variables.h +++ b/Zend/zend_variables.h @@ -78,6 +78,7 @@ static zend_always_inline void zval_ptr_dtor_str(zval *zval_ptr) } ZEND_API void zval_ptr_dtor(zval *zval_ptr); +ZEND_API void zval_ptr_safe_dtor(zval *zval_ptr); ZEND_API void zval_internal_ptr_dtor(zval *zvalue); /* Kept for compatibility */ From 229df24ae8dc2eaebd9e092be80e7a9975555fd6 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Tue, 7 Jan 2025 23:27:59 +0000 Subject: [PATCH 7/8] Zend/GC: Add zend_get_gc_buffer_add_ht() function --- Zend/zend_gc.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Zend/zend_gc.h b/Zend/zend_gc.h index a52de1bfcfa14..262d656c24ea8 100644 --- a/Zend/zend_gc.h +++ b/Zend/zend_gc.h @@ -141,6 +141,18 @@ static zend_always_inline void zend_get_gc_buffer_add_obj( gc_buffer->cur++; } +static zend_always_inline void zend_get_gc_buffer_add_ht( + zend_get_gc_buffer *gc_buffer, HashTable *ht) { + if (GC_FLAGS(ht) & IS_ARRAY_IMMUTABLE) { + return; + } + if (UNEXPECTED(gc_buffer->cur == gc_buffer->end)) { + zend_get_gc_buffer_grow(gc_buffer); + } + ZVAL_ARR(gc_buffer->cur, ht); + gc_buffer->cur++; +} + static zend_always_inline void zend_get_gc_buffer_add_ptr( zend_get_gc_buffer *gc_buffer, void *ptr) { if (UNEXPECTED(gc_buffer->cur == gc_buffer->end)) { From 3ff7758bcf7ebb05cbf3bc8ddb7ca177fa211725 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 9 Jan 2025 13:26:50 +0000 Subject: [PATCH 8/8] ext/pdo: Refactor PDO::FETCH_CLASS to not rely on a FCI and use a HashTable for ctor_arg To call the constructor we now only store the CE and a HashTable for the arguments. This reduces the size of the _pdo_stmt_t struct from 320 bytes to 232 bytes. Moreover, this now means that the constructor argument array follows the usual CUFA semantics. This change is a BC break, as string keys now act like named arguments. Moreover, the automatic wrapping of by-value arguments for by-ref parameters has been dropped, and the usual E_WARNING is now emitted in those cases. The do_fetch() is heavily refactored to simplify the execution flow, which also makes it easier to understand. Additionally we add a new bitflag in_fetch to prevent modification of the fetch flags by userland when PDO is fetching from the DB. --- UPGRADING | 12 + ext/pdo/pdo_stmt.c | 465 +++++++----------- ext/pdo/php_pdo_driver.h | 9 +- ext/pdo/php_pdo_error.h | 3 +- ext/pdo/tests/pdo_fetch_class_basic.phpt | 3 +- .../pdo_fetch_class_by_ref_constructor.phpt | 13 +- ..._class_change_ctor_args_during_fetch1.phpt | 14 +- ..._class_change_ctor_args_during_fetch2.phpt | 14 +- ..._class_change_ctor_args_during_fetch3.phpt | 41 +- ..._class_change_ctor_args_during_fetch4.phpt | 41 +- ..._class_change_ctor_args_during_fetch5.phpt | 13 +- ...fetch_class_ctor_with_named_arguments.phpt | 12 +- ...amed_arguments_positional_after_named.phpt | 38 +- ext/pdo/tests/pdo_stmt_class_ctor_errors.phpt | 4 +- .../tests/pdo_stmt_fetchobject_ctor_args.phpt | 18 +- ...t_fetchobject_ctor_args_after_default.phpt | 68 +++ 16 files changed, 342 insertions(+), 426 deletions(-) create mode 100644 ext/pdo/tests/pdo_stmt_fetchobject_ctor_args_after_default.phpt diff --git a/UPGRADING b/UPGRADING index fcf1703fa5d92..26b2dd0f84b0d 100644 --- a/UPGRADING +++ b/UPGRADING @@ -50,6 +50,18 @@ PHP 8.5 UPGRADE NOTES . pcntl_exec() now throws ValueErrors when entries or keys of the $env_vars parameter contain null bytes. +- PDO: + . The constructor arguments set in conjunction with PDO::FETCH_CLASS now + follow the usual CUFA (call_user_func_array) semantics. + This means string keys will act like a named argument. + Moreover, automatic wrapping for by-value arguments passed to a by-ref + parameter has been removed, and the usual E_WARNING about this is now + emitted. + To pass a variable by-ref to a constructor argument use the general + array value reference assignment: $ctor_args = [&$valByRef] + . Attempting to modify a PDOStatement during a call to PDO::fetch(), + PDO::fetchObject(), PDO::fetchAll() will now throw an Error. + - PDO_FIREBIRD: . A ValueError is now thrown when trying to set a cursor name that is too long on a PDOStatement resulting from the Firebird driver. diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c index cb9fdf957af76..f803dfe836b10 100644 --- a/ext/pdo/pdo_stmt.c +++ b/ext/pdo/pdo_stmt.c @@ -610,60 +610,6 @@ static bool do_fetch_common(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori, ze } /* }}} */ -static bool do_fetch_class_prepare(pdo_stmt_t *stmt) /* {{{ */ -{ - zend_class_entry *ce = stmt->fetch.cls.ce; - zend_fcall_info *fci = &stmt->fetch.cls.fci; - zend_fcall_info_cache *fcc = &stmt->fetch.cls.fcc; - - fci->size = sizeof(zend_fcall_info); - - if (!ce) { - stmt->fetch.cls.ce = ZEND_STANDARD_CLASS_DEF_PTR; - ce = ZEND_STANDARD_CLASS_DEF_PTR; - } - - if (ce->constructor) { - ZVAL_UNDEF(&fci->function_name); - fci->param_count = 0; - fci->params = NULL; - - zend_fcall_info_args_ex(fci, ce->constructor, &stmt->fetch.cls.ctor_args); - - fcc->function_handler = ce->constructor; - fcc->called_scope = ce; - return 1; - } else if (!Z_ISUNDEF(stmt->fetch.cls.ctor_args)) { - zend_throw_error(NULL, "User-supplied statement does not accept constructor arguments"); - return 0; - } else { - return 1; /* no ctor no args is also ok */ - } -} -/* }}} */ - -static void do_fetch_opt_finish(pdo_stmt_t *stmt, bool free_ctor_agrs) /* {{{ */ -{ - /* fci.size is used to check if it is valid */ - if (stmt->fetch.cls.fci.size && stmt->fetch.cls.fci.params) { - if (!Z_ISUNDEF(stmt->fetch.cls.ctor_args)) { - /* Added to free constructor arguments */ - zend_fcall_info_args_clear(&stmt->fetch.cls.fci, 1); - } else { - efree(stmt->fetch.cls.fci.params); - } - stmt->fetch.cls.fci.params = NULL; - } - - stmt->fetch.cls.fci.size = 0; - if (!Z_ISUNDEF(stmt->fetch.cls.ctor_args) && free_ctor_agrs) { - zval_ptr_dtor(&stmt->fetch.cls.ctor_args); - ZVAL_UNDEF(&stmt->fetch.cls.ctor_args); - stmt->fetch.cls.fci.param_count = 0; - } -} -/* }}} */ - static bool pdo_do_key_pair_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori, zend_long offset, HashTable *container) { if (!do_fetch_common(stmt, ori, offset)) { @@ -688,13 +634,41 @@ static bool pdo_do_key_pair_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation o return true; } -/* perform a fetch. - * Stores values into return_value according to HOW. */ +/* Return value MUST be an initialized object */ +static bool pdo_call_fetch_object_constructor(zend_function *constructor, HashTable *ctor_args, zval *return_value) +{ + zval retval_constructor_call; + zend_fcall_info fci = { + .size = sizeof(zend_fcall_info), + .function_name = {}, + .object = Z_OBJ_P(return_value), + .retval = &retval_constructor_call, + .param_count = 0, + .params = NULL, + .named_params = ctor_args, + }; + zend_fcall_info_cache fcc = { + .function_handler = constructor, + .object = Z_OBJ_P(return_value), + .called_scope = Z_OBJCE_P(return_value), + .calling_scope = NULL, + .closure = NULL, + }; + + zend_call_function(&fci, &fcc); + bool failed = Z_ISUNDEF(retval_constructor_call); + zval_ptr_dtor(&retval_constructor_call); + + return failed; +} + +/* Performs a row fetch, the value is stored into return_value according to HOW. + * retun_value MUST be safely destroyable as it will be freed if an error occurs. */ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type how, enum pdo_fetch_orientation ori, zend_long offset, zval *group_key) /* {{{ */ { - int flags, idx, old_arg_count = 0; - zend_class_entry *ce = NULL, *old_ce = NULL; - zval old_ctor_args = {{0}, {0}, {0}}; + int flags; + zend_class_entry *ce = NULL; + HashTable *ctor_arguments = NULL; int column_index_to_fetch = 0; zval *fetch_function_params = NULL; uint32_t fetch_function_param_num = 0; @@ -756,8 +730,7 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h return true; } - RETVAL_FALSE; - + stmt->in_fetch = true; switch (how) { case PDO_FETCH_USE_DEFAULT: case PDO_FETCH_ASSOC: @@ -772,90 +745,81 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h break; case PDO_FETCH_OBJ: - object_init_ex(return_value, ZEND_STANDARD_CLASS_DEF_PTR); + ce = zend_standard_class_def; + object_init(return_value); break; case PDO_FETCH_CLASS: + ce = stmt->fetch.cls.ce; if (flags & PDO_FETCH_CLASSTYPE) { - zval val; - zend_class_entry *cep; - - old_ce = stmt->fetch.cls.ce; - ZVAL_COPY_VALUE(&old_ctor_args, &stmt->fetch.cls.ctor_args); - old_arg_count = stmt->fetch.cls.fci.param_count; - do_fetch_opt_finish(stmt, 0); - - fetch_value(stmt, &val, column_index_to_fetch++, NULL); - if (Z_TYPE(val) != IS_NULL) { - if (!try_convert_to_string(&val)) { - return 0; - } - if ((cep = zend_lookup_class(Z_STR(val))) == NULL) { - stmt->fetch.cls.ce = ZEND_STANDARD_CLASS_DEF_PTR; - } else { - stmt->fetch.cls.ce = cep; - } + zval ce_name_from_column; + fetch_value(stmt, &ce_name_from_column, column_index_to_fetch++, NULL); + /* This used to use try_convert_to_string() which would silently support integers, floats, null + * even if any such value could not generate a valid class name, as no class was found it would + * then proceed to use stdClass */ + // TODO Raise PDO implementation error when the column name is not a string + if (Z_TYPE(ce_name_from_column) == IS_STRING) { + ce = zend_lookup_class(Z_STR(ce_name_from_column)); } - - do_fetch_class_prepare(stmt); - zval_ptr_dtor_str(&val); - } - ce = stmt->fetch.cls.ce; - /* TODO: Make this an assertion and ensure this is true higher up? */ - if (!ce) { - /* TODO Error? */ - pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "No fetch class specified"); - return 0; + /* Use default CE if present */ + if (ce == NULL) { + ce = zend_standard_class_def; + } + zval_ptr_dtor(&ce_name_from_column); + } else { + /* This can happen if the fetch flags are set via PDO::setAttribute() + * $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_CLASS); + * See ext/pdo/tests/bug_38253.phpt */ + if (UNEXPECTED(ce == NULL)) { + pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "No fetch class specified"); + goto in_fetch_error; + } + ctor_arguments = stmt->fetch.cls.ctor_args; } - if ((flags & PDO_FETCH_SERIALIZE) == 0) { - if (UNEXPECTED(object_init_ex(return_value, ce) != SUCCESS)) { - return 0; + ZEND_ASSERT(ce != NULL); + if (flags & PDO_FETCH_SERIALIZE) { + if (!ce->unserialize) { + /* As this option is deprecated we do not bother to mention the class name. */ + pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "cannot unserialize class"); + goto in_fetch_error; } - if (!stmt->fetch.cls.fci.size) { - if (!do_fetch_class_prepare(stmt)) { - zval_ptr_dtor(return_value); - return 0; - } + } else { + if (UNEXPECTED(object_init_ex(return_value, ce) != SUCCESS)) { + goto in_fetch_error; } if (ce->constructor && (flags & PDO_FETCH_PROPS_LATE)) { - zval retval_constructor_call; - stmt->fetch.cls.fci.retval = &retval_constructor_call; - stmt->fetch.cls.fci.object = Z_OBJ_P(return_value); - stmt->fetch.cls.fcc.object = Z_OBJ_P(return_value); - zend_call_function(&stmt->fetch.cls.fci, &stmt->fetch.cls.fcc); - if (Z_TYPE(retval_constructor_call) == IS_UNDEF) { - /* Exception has happened */ + bool failed = pdo_call_fetch_object_constructor(ce->constructor, ctor_arguments, return_value); + if (UNEXPECTED(failed)) { zval_ptr_dtor(return_value); - return false; + goto in_fetch_error; } - zval_ptr_dtor(&retval_constructor_call); - ZVAL_UNDEF(stmt->fetch.cls.fci.retval); } } break; case PDO_FETCH_INTO: - /* TODO: Make this an assertion and ensure this is true higher up? */ + /* This can happen if the fetch flags are set via PDO::setAttribute() + * $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_INTO); + * See ext/pdo/tests/bug_38253.phpt */ if (stmt->fetch.into == NULL) { - /* TODO ArgumentCountError? */ pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "No fetch-into object specified."); - return 0; - break; + goto in_fetch_error; } ZVAL_OBJ_COPY(return_value, stmt->fetch.into); - if (Z_OBJ_P(return_value)->ce == ZEND_STANDARD_CLASS_DEF_PTR) { - how = PDO_FETCH_OBJ; - } + /* We want the behaviour of fetching into an object to be called from the global scope rather + * than the object scope */ + ce = NULL; break; case PDO_FETCH_FUNC: - /* TODO: Make this an assertion and ensure this is true higher up? */ - if (!ZEND_FCC_INITIALIZED(stmt->fetch.func.fcc)) { - /* TODO ArgumentCountError? */ + /* This can happen if the fetch flags are set via PDO::setAttribute() + * $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_FUNC); + * See ext/pdo/tests/bug_38253.phpt */ + if (UNEXPECTED(!ZEND_FCC_INITIALIZED(stmt->fetch.func.fcc))) { pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "No fetch function specified"); - return false; + goto in_fetch_error; } /* There will be at most stmt->column_count parameters. * However, if we fetch a group key we will have over allocated. */ @@ -870,7 +834,27 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h column_index_to_fetch++; } - for (idx = 0; column_index_to_fetch < stmt->column_count; column_index_to_fetch++, idx++) { + if (how == PDO_FETCH_CLASS && (flags & PDO_FETCH_SERIALIZE)) { + zval unserialization_string; + fetch_value(stmt, &unserialization_string, column_index_to_fetch, NULL); + + const unsigned char *str = (const unsigned char*) ""; + size_t str_len = 0; + if (Z_TYPE(unserialization_string) == IS_STRING) { + str = (unsigned char*) Z_STRVAL(unserialization_string); + str_len = Z_STRLEN(unserialization_string); + } + zend_result unserialize_res = ce->unserialize(return_value, ce, str, str_len, NULL); + zval_ptr_dtor(&unserialization_string); + if (unserialize_res == FAILURE) { + pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "cannot unserialize class"); + zval_ptr_dtor(return_value); + goto in_fetch_error; + } + column_index_to_fetch++; + } + + for (; column_index_to_fetch < stmt->column_count; column_index_to_fetch++) { zval val; fetch_value(stmt, &val, column_index_to_fetch, NULL); zend_string *column_name = stmt->columns[column_index_to_fetch].name; @@ -926,82 +910,41 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &val); break; + case PDO_FETCH_CLASS: case PDO_FETCH_OBJ: case PDO_FETCH_INTO: - zend_update_property_ex(NULL, Z_OBJ_P(return_value), column_name, &val); + zend_update_property_ex(ce, Z_OBJ_P(return_value), column_name, &val); zval_ptr_dtor(&val); break; - case PDO_FETCH_CLASS: - if ((flags & PDO_FETCH_SERIALIZE) == 0 || idx) { - zend_update_property_ex(ce, Z_OBJ_P(return_value), column_name, &val); - zval_ptr_dtor(&val); - } else { - if (!ce->unserialize) { - zval_ptr_dtor(&val); - pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "cannot unserialize class"); - return 0; - } else if (ce->unserialize(return_value, ce, (unsigned char *)(Z_TYPE(val) == IS_STRING ? Z_STRVAL(val) : ""), Z_TYPE(val) == IS_STRING ? Z_STRLEN(val) : 0, NULL) == FAILURE) { - zval_ptr_dtor(&val); - pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "cannot unserialize class"); - zval_ptr_dtor(return_value); - ZVAL_NULL(return_value); - return 0; - } else { - zval_ptr_dtor(&val); - } - } - break; - case PDO_FETCH_FUNC: ZVAL_COPY_VALUE(&fetch_function_params[fetch_function_param_num++], &val); break; - - default: - zval_ptr_dtor(&val); - zend_value_error("Fetch mode must be a bitmask of PDO::FETCH_* constants"); - return 0; + EMPTY_SWITCH_DEFAULT_CASE(); } } - switch (how) { - case PDO_FETCH_CLASS: - if (ce->constructor && !(flags & (PDO_FETCH_PROPS_LATE | PDO_FETCH_SERIALIZE))) { - zval retval_constructor_call; - stmt->fetch.cls.fci.retval = &retval_constructor_call; - stmt->fetch.cls.fci.object = Z_OBJ_P(return_value); - stmt->fetch.cls.fcc.object = Z_OBJ_P(return_value); - zend_call_function(&stmt->fetch.cls.fci, &stmt->fetch.cls.fcc); - if (Z_TYPE(retval_constructor_call) == IS_UNDEF) { - /* Exception has happened */ - zval_ptr_dtor(return_value); - return false; - } - zval_ptr_dtor(&retval_constructor_call); - ZVAL_UNDEF(stmt->fetch.cls.fci.retval); - } - if (flags & PDO_FETCH_CLASSTYPE) { - do_fetch_opt_finish(stmt, 0); - stmt->fetch.cls.ce = old_ce; - ZVAL_COPY_VALUE(&stmt->fetch.cls.ctor_args, &old_ctor_args); - stmt->fetch.cls.fci.param_count = old_arg_count; - } - break; - - case PDO_FETCH_FUNC: - zend_call_known_fcc(&stmt->fetch.func.fcc, return_value, fetch_function_param_num, fetch_function_params, NULL); - /* Free FCI parameters that were allocated in the previous loop */ - for (uint32_t param_num = 0; param_num < fetch_function_param_num; param_num++) { - zval_ptr_dtor(&fetch_function_params[param_num]); - } - efree(fetch_function_params); - break; - - default: - break; + /* Run constructor for objects if not already run and not unserialized */ + if (how == PDO_FETCH_CLASS && ce->constructor && !(flags & (PDO_FETCH_PROPS_LATE | PDO_FETCH_SERIALIZE))) { + bool failed = pdo_call_fetch_object_constructor(ce->constructor, ctor_arguments, return_value); + if (UNEXPECTED(failed)) { + zval_ptr_dtor(return_value); + goto in_fetch_error; + } + } else if (how == PDO_FETCH_FUNC) { + zend_call_known_fcc(&stmt->fetch.func.fcc, return_value, fetch_function_param_num, fetch_function_params, NULL); + /* Free FCI parameters that were allocated in the previous loop */ + for (uint32_t param_num = 0; param_num < fetch_function_param_num; param_num++) { + zval_ptr_dtor(&fetch_function_params[param_num]); + } + efree(fetch_function_params); } + stmt->in_fetch = false; + return true; - return 1; +in_fetch_error: + stmt->in_fetch = false; + return false; } /* }}} */ @@ -1108,44 +1051,38 @@ PHP_METHOD(PDOStatement, fetchObject) { zend_class_entry *ce = NULL; zend_class_entry *old_ce; - zval old_ctor_args, *ctor_args = NULL; - int old_arg_count; + HashTable *old_ctor_args, *ctor_args = NULL; ZEND_PARSE_PARAMETERS_START(0, 2) Z_PARAM_OPTIONAL Z_PARAM_CLASS_OR_NULL(ce) - Z_PARAM_ARRAY(ctor_args) + Z_PARAM_ARRAY_HT(ctor_args) ZEND_PARSE_PARAMETERS_END(); PHP_STMT_GET_OBJ; PDO_STMT_CLEAR_ERR(); old_ce = stmt->fetch.cls.ce; - ZVAL_COPY_VALUE(&old_ctor_args, &stmt->fetch.cls.ctor_args); - old_arg_count = stmt->fetch.cls.fci.param_count; + old_ctor_args = stmt->fetch.cls.ctor_args; - do_fetch_opt_finish(stmt, 0); - - if (ctor_args && zend_hash_num_elements(Z_ARRVAL_P(ctor_args))) { - ZVAL_ARR(&stmt->fetch.cls.ctor_args, zend_array_dup(Z_ARRVAL_P(ctor_args))); - } else { - ZVAL_UNDEF(&stmt->fetch.cls.ctor_args); + if (ce == NULL) { + ce = zend_standard_class_def; } - if (ce) { - stmt->fetch.cls.ce = ce; - } else { - stmt->fetch.cls.ce = zend_standard_class_def; + + if (ctor_args && zend_hash_num_elements(ctor_args) && ce->constructor == NULL) { + zend_argument_value_error(2, "must be empty when class provided in argument #1 ($class) does not have a constructor"); + RETURN_THROWS(); } + stmt->fetch.cls.ce = ce; + stmt->fetch.cls.ctor_args = ctor_args; if (!do_fetch(stmt, return_value, PDO_FETCH_CLASS, PDO_FETCH_ORI_NEXT, /* offset */ 0, NULL)) { PDO_HANDLE_STMT_ERR(); RETVAL_FALSE; } - do_fetch_opt_finish(stmt, 1); stmt->fetch.cls.ce = old_ce; - ZVAL_COPY_VALUE(&stmt->fetch.cls.ctor_args, &old_ctor_args); - stmt->fetch.cls.fci.param_count = old_arg_count; + stmt->fetch.cls.ctor_args = old_ctor_args; } /* }}} */ @@ -1197,15 +1134,13 @@ PHP_METHOD(PDOStatement, fetchAll) zend_long how = PDO_FETCH_USE_DEFAULT; zval *arg2 = NULL; zend_class_entry *old_ce; - zval old_ctor_args, *ctor_args = NULL; - uint32_t old_arg_count; - HashTable *current_ctor = NULL; + HashTable *old_ctor_args, *ctor_args = NULL; ZEND_PARSE_PARAMETERS_START(0, 3) Z_PARAM_OPTIONAL Z_PARAM_LONG(how) Z_PARAM_ZVAL_OR_NULL(arg2) - Z_PARAM_ARRAY_OR_NULL(ctor_args) + Z_PARAM_ARRAY_HT_OR_NULL(ctor_args) ZEND_PARSE_PARAMETERS_END(); PHP_STMT_GET_OBJ; @@ -1217,48 +1152,38 @@ PHP_METHOD(PDOStatement, fetchAll) int flags = how & PDO_FETCH_FLAGS; old_ce = stmt->fetch.cls.ce; - ZVAL_COPY_VALUE(&old_ctor_args, &stmt->fetch.cls.ctor_args); - if (Z_TYPE(old_ctor_args) == IS_ARRAY) { - /* Protect against destruction by marking this as immutable: we consider this non-owned temporarily */ - Z_TYPE_INFO(stmt->fetch.cls.ctor_args) = IS_ARRAY; - } - old_arg_count = stmt->fetch.cls.fci.param_count; - - do_fetch_opt_finish(stmt, 0); + old_ctor_args = stmt->fetch.cls.ctor_args; /* TODO Would be good to reuse part of pdo_stmt_setup_fetch_mode() in some way */ - switch (fetch_mode) { - case PDO_FETCH_CLASS: + case PDO_FETCH_CLASS: { /* Figure out correct class */ + zend_class_entry *fetch_class = NULL; if (arg2) { if (Z_TYPE_P(arg2) != IS_STRING) { zend_argument_type_error(2, "must be of type string, %s given", zend_zval_value_name(arg2)); RETURN_THROWS(); } - stmt->fetch.cls.ce = zend_lookup_class(Z_STR_P(arg2)); - if (!stmt->fetch.cls.ce) { + fetch_class = zend_lookup_class(Z_STR_P(arg2)); + if (fetch_class == NULL) { zend_argument_type_error(2, "must be a valid class"); RETURN_THROWS(); } } else { - stmt->fetch.cls.ce = zend_standard_class_def; + fetch_class = zend_standard_class_def; } - if (ctor_args && zend_hash_num_elements(Z_ARRVAL_P(ctor_args)) > 0) { - /* We increase the refcount and store it in case usercode has been messing around with the ctor args. - * We need to store current_ctor separately as usercode may change the ctor_args which will cause a leak. */ - current_ctor = Z_ARRVAL_P(ctor_args); - ZVAL_COPY(&stmt->fetch.cls.ctor_args, ctor_args); - /* Protect against destruction by marking this as immutable: we consider this non-owned - * as destruction is handled via current_ctor. */ - Z_TYPE_INFO(stmt->fetch.cls.ctor_args) = IS_ARRAY; - } else { - ZVAL_UNDEF(&stmt->fetch.cls.ctor_args); + if (ctor_args && zend_hash_num_elements(ctor_args) > 0) { + if (fetch_class->constructor == NULL) { + zend_argument_value_error(3, "must be empty when class provided in argument #2 ($class) does not have a constructor"); + RETURN_THROWS(); + } + stmt->fetch.cls.ctor_args = ctor_args; } + stmt->fetch.cls.ce = fetch_class; - do_fetch_class_prepare(stmt); break; + } case PDO_FETCH_FUNC: /* Cannot be a default fetch mode */ if (ZEND_NUM_ARGS() != 2) { @@ -1309,7 +1234,6 @@ PHP_METHOD(PDOStatement, fetchAll) } } - if (fetch_mode == PDO_FETCH_USE_DEFAULT) { flags |= stmt->default_fetch_type & PDO_FETCH_FLAGS; fetch_mode = stmt->default_fetch_type & ~PDO_FETCH_FLAGS; @@ -1320,6 +1244,7 @@ PHP_METHOD(PDOStatement, fetchAll) zval data, group_key; + ZVAL_UNDEF(&data); array_init(return_value); if (fetch_mode == PDO_FETCH_KEY_PAIR) { @@ -1353,20 +1278,11 @@ PHP_METHOD(PDOStatement, fetchAll) } } - do_fetch_opt_finish(stmt, 0); - if (current_ctor) { - zend_array_release(current_ctor); - } - /* Restore defaults which were changed by PDO_FETCH_CLASS mode */ stmt->fetch.cls.ce = old_ce; - /* ctor_args may have been changed to an owned object in the meantime, so destroy it. - * If it was not, then the type flags update will have protected us against destruction. */ - zval_ptr_dtor(&stmt->fetch.cls.ctor_args); - ZVAL_COPY_VALUE(&stmt->fetch.cls.ctor_args, &old_ctor_args); - stmt->fetch.cls.fci.param_count = old_arg_count; + stmt->fetch.cls.ctor_args = old_ctor_args; - PDO_HANDLE_STMT_ERR(); + PDO_HANDLE_STMT_ERR_EX(zval_ptr_dtor(return_value); RETVAL_EMPTY_ARRAY();); } /* }}} */ @@ -1660,6 +1576,24 @@ PHP_METHOD(PDOStatement, getColumnMeta) } /* }}} */ +void pdo_stmt_free_default_fetch_mode(pdo_stmt_t *stmt) +{ + enum pdo_fetch_type default_fetch_mode = stmt->default_fetch_type & ~PDO_FETCH_FLAGS; + if (default_fetch_mode == PDO_FETCH_INTO) { + /* This can happen if the fetch flags are set via PDO::setAttribute() + * $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_INTO); + * See ext/pdo/tests/bug_38253.phpt */ + if (EXPECTED(stmt->fetch.into != NULL)) { + OBJ_RELEASE(stmt->fetch.into); + } + } else if (default_fetch_mode == PDO_FETCH_CLASS) { + if (stmt->fetch.cls.ctor_args != NULL) { + zend_array_release(stmt->fetch.cls.ctor_args); + } + } + memset(&stmt->fetch, 0, sizeof(stmt->fetch)); +} + /* {{{ Changes the default fetch mode for subsequent fetches (params have different meaning for different fetch modes) */ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_arg_num, @@ -1670,16 +1604,7 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a uint32_t constructor_arg_num = mode_arg_num + 2; uint32_t total_num_args = mode_arg_num + variadic_num_args; - switch (stmt->default_fetch_type) { - case PDO_FETCH_INTO: - if (stmt->fetch.into) { - OBJ_RELEASE(stmt->fetch.into); - stmt->fetch.into = NULL; - } - break; - default: - ; - } + pdo_stmt_free_default_fetch_mode(stmt); stmt->default_fetch_type = PDO_FETCH_BOTH; @@ -1728,9 +1653,6 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a break; case PDO_FETCH_CLASS: { - HashTable *constructor_args = NULL; - /* Undef constructor arguments */ - ZVAL_UNDEF(&stmt->fetch.cls.ctor_args); /* Gets its class name from 1st column */ if ((flags & PDO_FETCH_CLASSTYPE) == PDO_FETCH_CLASSTYPE) { if (variadic_num_args != 0) { @@ -1740,7 +1662,6 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a zend_string_release(func); return false; } - stmt->fetch.cls.ce = NULL; } else { zend_class_entry *cep; if (variadic_num_args == 0) { @@ -1776,18 +1697,16 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a return false; } if (Z_TYPE(args[1]) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL(args[1]))) { - constructor_args = Z_ARRVAL(args[1]); + if (cep->constructor == NULL) { + zend_argument_value_error(3, "must be empty when class provided in argument #2 ($class) does not have a constructor"); + return false; + } + GC_TRY_ADDREF(Z_ARRVAL(args[1])); + stmt->fetch.cls.ctor_args = Z_ARRVAL(args[1]); } } stmt->fetch.cls.ce = cep; - - /* If constructor arguments are present and not empty */ - if (constructor_args) { - ZVAL_ARR(&stmt->fetch.cls.ctor_args, zend_array_dup(constructor_args)); - } } - - do_fetch_class_prepare(stmt); break; } case PDO_FETCH_INTO: @@ -1828,8 +1747,10 @@ PHP_METHOD(PDOStatement, setFetchMode) PHP_STMT_GET_OBJ; - do_fetch_opt_finish(stmt, 1); - + if (stmt->in_fetch) { + zend_throw_error(NULL, "Cannot change default fetch mode while fetching"); + RETURN_THROWS(); + } if (!pdo_stmt_setup_fetch_mode(stmt, fetch_mode, 1, args, num_args)) { RETURN_THROWS(); } @@ -2051,8 +1972,8 @@ static HashTable *dbstmt_get_gc(zend_object *object, zval **gc_data, int *gc_cou zend_get_gc_buffer_add_obj(gc_buffer, stmt->database_object_handle); if (default_fetch_mode == PDO_FETCH_INTO) { zend_get_gc_buffer_add_obj(gc_buffer, stmt->fetch.into); - } else if (default_fetch_mode == PDO_FETCH_CLASS) { - zend_get_gc_buffer_add_zval(gc_buffer, &stmt->fetch.cls.ctor_args); + } else if (default_fetch_mode == PDO_FETCH_CLASS && stmt->fetch.cls.ctor_args != NULL) { + zend_get_gc_buffer_add_ht(gc_buffer, stmt->fetch.cls.ctor_args); } zend_get_gc_buffer_use(gc_buffer, gc_data, gc_count); @@ -2099,13 +2020,7 @@ PDO_API void php_pdo_free_statement(pdo_stmt_t *stmt) } pdo_stmt_reset_columns(stmt); - - if (stmt->fetch.into && stmt->default_fetch_type == PDO_FETCH_INTO) { - OBJ_RELEASE(stmt->fetch.into); - stmt->fetch.into = NULL; - } - - do_fetch_opt_finish(stmt, 1); + pdo_stmt_free_default_fetch_mode(stmt); if (stmt->database_object_handle != NULL) { OBJ_RELEASE(stmt->database_object_handle); @@ -2148,6 +2063,7 @@ static void pdo_stmt_iter_dtor(zend_object_iterator *iter) if (!Z_ISUNDEF(I->fetch_ahead)) { zval_ptr_dtor(&I->fetch_ahead); + ZVAL_UNDEF(&I->fetch_ahead); } } @@ -2188,6 +2104,7 @@ static void pdo_stmt_iter_move_forwards(zend_object_iterator *iter) if (!Z_ISUNDEF(I->fetch_ahead)) { zval_ptr_dtor(&I->fetch_ahead); + ZVAL_UNDEF(&I->fetch_ahead); } if (!do_fetch(stmt, &I->fetch_ahead, PDO_FETCH_USE_DEFAULT, diff --git a/ext/pdo/php_pdo_driver.h b/ext/pdo/php_pdo_driver.h index 94e7c805bfde8..ad5a1c1340b0a 100644 --- a/ext/pdo/php_pdo_driver.h +++ b/ext/pdo/php_pdo_driver.h @@ -568,7 +568,9 @@ struct _pdo_stmt_t { * emulate prepare and bind on its behalf */ unsigned supports_placeholders:2; - unsigned _reserved:29; + /* If true we are in a do_fetch() call, and modification to the statement must be prevented */ + unsigned in_fetch:1; + unsigned _reserved:28; /* the number of columns in the result set; not valid until after * the statement has been executed at least once. In some cases, might @@ -611,13 +613,10 @@ struct _pdo_stmt_t { union { int column; struct { - zval ctor_args; /* freed */ - zend_fcall_info_cache fcc; - zend_fcall_info fci; + HashTable *ctor_args; zend_class_entry *ce; } cls; struct { - zval dummy; /* This exists due to alignment reasons with fetch.into and fetch.cls.ctor_args */ zend_fcall_info_cache fcc; } func; zend_object *into; diff --git a/ext/pdo/php_pdo_error.h b/ext/pdo/php_pdo_error.h index 852bfab6f6739..41759b2d5ccc1 100644 --- a/ext/pdo/php_pdo_error.h +++ b/ext/pdo/php_pdo_error.h @@ -34,6 +34,7 @@ PDO_API void pdo_handle_error(pdo_dbh_t *dbh, pdo_stmt_t *stmt); memcpy(stmt->error_code, PDO_ERR_NONE, sizeof(PDO_ERR_NONE)); \ } while (0) #define PDO_HANDLE_DBH_ERR() if (strcmp(dbh->error_code, PDO_ERR_NONE)) { pdo_handle_error(dbh, NULL); } -#define PDO_HANDLE_STMT_ERR() if (strcmp(stmt->error_code, PDO_ERR_NONE)) { pdo_handle_error(stmt->dbh, stmt); } +#define PDO_HANDLE_STMT_ERR_EX(cleanup_instruction) if (strcmp(stmt->error_code, PDO_ERR_NONE) != 0) { cleanup_instruction pdo_handle_error(stmt->dbh, stmt); } +#define PDO_HANDLE_STMT_ERR() PDO_HANDLE_STMT_ERR_EX(;) #endif /* PHP_PDO_ERROR_H */ diff --git a/ext/pdo/tests/pdo_fetch_class_basic.phpt b/ext/pdo/tests/pdo_fetch_class_basic.phpt index b22e4aa9a9b5b..5ec19bcb249ec 100644 --- a/ext/pdo/tests/pdo_fetch_class_basic.phpt +++ b/ext/pdo/tests/pdo_fetch_class_basic.phpt @@ -48,7 +48,8 @@ $stmt->execute(); var_dump($stmt->fetchAll(PDO::FETCH_CLASS, 'TestBase')); $stmt->execute(); -var_dump($stmt->fetchAll(PDO::FETCH_CLASS, 'TestDerived', array(0))); +$rowCounter = 0; +var_dump($stmt->fetchAll(PDO::FETCH_CLASS, 'TestDerived', [&$rowCounter])); ?> --CLEAN-- diff --git a/ext/pdo/tests/pdo_fetch_class_by_ref_constructor.phpt b/ext/pdo/tests/pdo_fetch_class_by_ref_constructor.phpt index 7523a6cf0b764..4524980b2c4a7 100644 --- a/ext/pdo/tests/pdo_fetch_class_by_ref_constructor.phpt +++ b/ext/pdo/tests/pdo_fetch_class_by_ref_constructor.phpt @@ -47,9 +47,14 @@ $db = PDOTest::factory(); PDOTest::dropTableIfExists($db, "pdo_fetch_class_by_ref_ctor"); ?> --EXPECTF-- +Warning: TestByRefCtor::__construct(): Argument #1 ($str) must be passed by reference, value given in %s on line %d TestByRefCtor::__construct(aaaaaaaaaa, 1) -TestByRefCtor::__construct(aaaaaaaaaaA, 2) -TestByRefCtor::__construct(aaaaaaaaaaAB, 3) + +Warning: TestByRefCtor::__construct(): Argument #1 ($str) must be passed by reference, value given in %s on line %d +TestByRefCtor::__construct(aaaaaaaaaa, 2) + +Warning: TestByRefCtor::__construct(): Argument #1 ($str) must be passed by reference, value given in %s on line %d +TestByRefCtor::__construct(aaaaaaaaaa, 3) array(3) { [0]=> object(TestByRefCtor)#%d (3) { @@ -67,7 +72,7 @@ array(3) { ["val"]=> string(1) "B" ["str":"TestByRefCtor":private]=> - string(12) "aaaaaaaaaaAB" + string(11) "aaaaaaaaaaB" } [2]=> object(TestByRefCtor)#%d (3) { @@ -76,6 +81,6 @@ array(3) { ["val"]=> string(1) "C" ["str":"TestByRefCtor":private]=> - string(13) "aaaaaaaaaaABC" + string(11) "aaaaaaaaaaC" } } diff --git a/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch1.phpt b/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch1.phpt index 194610b1119a2..b9d798a241786 100644 --- a/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch1.phpt +++ b/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch1.phpt @@ -37,7 +37,12 @@ $stmt = $db->prepare('SELECT val1, val2 FROM pdo_fetch_class_change_ctor_one'); $stmt->setFetchMode(PDO::FETCH_CLASS, 'Test', [$stmt]); $stmt->execute(); -var_dump($stmt->fetch()); + +try { + var_dump($stmt->fetch()); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} ?> --CLEAN-- @@ -51,9 +56,4 @@ object(PDOStatement)#%d (1) { ["queryString"]=> string(54) "SELECT val1, val2 FROM pdo_fetch_class_change_ctor_one" } -object(Test)#%d (2) { - ["val1"]=> - string(1) "A" - ["val2"]=> - string(5) "alpha" -} +Error: Cannot change default fetch mode while fetching diff --git a/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch2.phpt b/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch2.phpt index f2cc4a2946997..23b91210d9dab 100644 --- a/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch2.phpt +++ b/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch2.phpt @@ -37,7 +37,12 @@ $db->exec("INSERT INTO pdo_fetch_class_change_ctor_two VALUES(4, 'D', 'delta')") $stmt = $db->prepare('SELECT val1, val2 FROM pdo_fetch_class_change_ctor_two'); $stmt->execute(); -var_dump($stmt->fetchObject('Test', [$stmt])); + +try { + var_dump($stmt->fetchObject('Test', [$stmt])); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} ?> --CLEAN-- @@ -51,9 +56,4 @@ object(PDOStatement)#%s (1) { ["queryString"]=> string(54) "SELECT val1, val2 FROM pdo_fetch_class_change_ctor_two" } -object(Test)#%s (2) { - ["val1"]=> - string(1) "A" - ["val2"]=> - string(5) "alpha" -} +Error: Cannot change default fetch mode while fetching diff --git a/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch3.phpt b/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch3.phpt index 6b2fb788700e8..120a6ce758119 100644 --- a/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch3.phpt +++ b/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch3.phpt @@ -37,7 +37,12 @@ $stmt = $db->prepare('SELECT val1, val2 FROM pdo_fetch_class_change_ctor_three') $stmt->setFetchMode(PDO::FETCH_CLASS, 'Test', [$stmt]); $stmt->execute(); -var_dump($stmt->fetchAll()); + +try { + var_dump($stmt->fetchAll()); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} ?> --CLEAN-- @@ -51,36 +56,4 @@ object(PDOStatement)#%d (1) { ["queryString"]=> string(56) "SELECT val1, val2 FROM pdo_fetch_class_change_ctor_three" } -string(5) "alpha" -string(5) "alpha" -string(5) "alpha" -array(4) { - [0]=> - object(Test)#%d (2) { - ["val1"]=> - string(1) "A" - ["val2"]=> - string(5) "alpha" - } - [1]=> - object(Test)#%d (2) { - ["val1"]=> - string(1) "B" - ["val2"]=> - string(4) "beta" - } - [2]=> - object(Test)#%d (2) { - ["val1"]=> - string(1) "C" - ["val2"]=> - string(5) "gamma" - } - [3]=> - object(Test)#%d (2) { - ["val1"]=> - string(1) "D" - ["val2"]=> - string(5) "delta" - } -} +Error: Cannot change default fetch mode while fetching diff --git a/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch4.phpt b/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch4.phpt index d4bad52a2b5ab..853f5f6db8651 100644 --- a/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch4.phpt +++ b/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch4.phpt @@ -36,7 +36,12 @@ $db->exec("INSERT INTO pdo_fetch_class_change_ctor_four VALUES(4, 'D', 'delta')" $stmt = $db->prepare('SELECT val1, val2 FROM pdo_fetch_class_change_ctor_four'); $stmt->execute(); -var_dump($stmt->fetchAll(PDO::FETCH_CLASS, 'Test', [$stmt])); + +try { + var_dump($stmt->fetchAll(PDO::FETCH_CLASS, 'Test', [$stmt])); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} ?> --CLEAN-- @@ -50,36 +55,4 @@ object(PDOStatement)#%d (1) { ["queryString"]=> string(55) "SELECT val1, val2 FROM pdo_fetch_class_change_ctor_four" } -string(5) "alpha" -string(5) "alpha" -string(5) "alpha" -array(4) { - [0]=> - object(Test)#%d (2) { - ["val1"]=> - string(1) "A" - ["val2"]=> - string(5) "alpha" - } - [1]=> - object(Test)#%d (2) { - ["val1"]=> - string(1) "B" - ["val2"]=> - string(4) "beta" - } - [2]=> - object(Test)#%d (2) { - ["val1"]=> - string(1) "C" - ["val2"]=> - string(5) "gamma" - } - [3]=> - object(Test)#%d (2) { - ["val1"]=> - string(1) "D" - ["val2"]=> - string(5) "delta" - } -} +Error: Cannot change default fetch mode while fetching diff --git a/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch5.phpt b/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch5.phpt index 533a61410c42e..c4b854023ffcd 100644 --- a/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch5.phpt +++ b/ext/pdo/tests/pdo_fetch_class_change_ctor_args_during_fetch5.phpt @@ -38,7 +38,11 @@ function stuffingErrorHandler(int $errno, string $errstr, string $errfile, int $ } set_error_handler(stuffingErrorHandler(...)); -var_dump($stmt->fetchAll(PDO::FETCH_CLASS|PDO::FETCH_SERIALIZE, 'B', [$stmt])); +try { + var_dump($stmt->fetchAll(PDO::FETCH_CLASS|PDO::FETCH_SERIALIZE, 'B', [$stmt])); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} ?> --CLEAN-- @@ -47,9 +51,6 @@ require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc'; $db = PDOTest::factory(); PDOTest::dropTableIfExists($db, "pdo_fetch_class_change_ctor_five"); ?> ---EXPECTF-- +--EXPECT-- PDOStatement::fetchAll(): The PDO::FETCH_SERIALIZE mode is deprecated -PDOStatement::fetchAll(): SQLSTATE[HY000]: General error: cannot unserialize class -PDOStatement::fetchAll(): SQLSTATE[HY000]: General error%S -array(0) { -} +Error: Cannot change default fetch mode while fetching diff --git a/ext/pdo/tests/pdo_fetch_class_ctor_with_named_arguments.phpt b/ext/pdo/tests/pdo_fetch_class_ctor_with_named_arguments.phpt index 95ded7d0b7606..8a7d205d2d8ee 100644 --- a/ext/pdo/tests/pdo_fetch_class_ctor_with_named_arguments.phpt +++ b/ext/pdo/tests/pdo_fetch_class_ctor_with_named_arguments.phpt @@ -44,12 +44,12 @@ $db = PDOTest::factory(); PDOTest::dropTableIfExists($db, "pdo_fetch_class_ctor_named"); ?> --EXPECTF-- -Value of $a: My key is B -Value of $b: My key is A -Value of $a: My key is B -Value of $b: My key is A -Value of $a: My key is B -Value of $b: My key is A +Value of $a: My key is A +Value of $b: My key is B +Value of $a: My key is A +Value of $b: My key is B +Value of $a: My key is A +Value of $b: My key is B array(3) { [0]=> object(TestBase)#%d (3) { diff --git a/ext/pdo/tests/pdo_fetch_class_ctor_with_named_arguments_positional_after_named.phpt b/ext/pdo/tests/pdo_fetch_class_ctor_with_named_arguments_positional_after_named.phpt index 7d36a022bd210..ff35dd7bd504e 100644 --- a/ext/pdo/tests/pdo_fetch_class_ctor_with_named_arguments_positional_after_named.phpt +++ b/ext/pdo/tests/pdo_fetch_class_ctor_with_named_arguments_positional_after_named.phpt @@ -48,39 +48,5 @@ require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc'; $db = PDOTest::factory(); PDOTest::dropTableIfExists($db, "pdo_fetch_class_ctor_named_and_positional"); ?> ---EXPECTF-- -Value of $a: My key is B -Value of $b: No key -Value of $a: My key is B -Value of $b: No key -Value of $a: My key is B -Value of $b: No key -array(3) { - [0]=> - object(TestBase)#%d (3) { - ["id"]=> - string(1) "1" - ["val":protected]=> - string(1) "A" - ["val2":"TestBase":private]=> - string(2) "AA" - } - [1]=> - object(TestBase)#%d (3) { - ["id"]=> - string(1) "2" - ["val":protected]=> - string(1) "B" - ["val2":"TestBase":private]=> - string(2) "BB" - } - [2]=> - object(TestBase)#%d (3) { - ["id"]=> - string(1) "3" - ["val":protected]=> - string(1) "C" - ["val2":"TestBase":private]=> - string(2) "CC" - } -} +--EXPECT-- +Error: Cannot use positional argument after named argument diff --git a/ext/pdo/tests/pdo_stmt_class_ctor_errors.phpt b/ext/pdo/tests/pdo_stmt_class_ctor_errors.phpt index 3a2984e0b3ce8..85fdbbec37f24 100644 --- a/ext/pdo/tests/pdo_stmt_class_ctor_errors.phpt +++ b/ext/pdo/tests/pdo_stmt_class_ctor_errors.phpt @@ -47,5 +47,5 @@ $db = PDOTest::factory(); PDOTest::dropTableIfExists($db, "pdo_fetch_all_class_ctor_error"); ?> --EXPECT-- -Error: User-supplied statement does not accept constructor arguments -Error: User-supplied statement does not accept constructor arguments +ValueError: PDOStatement::fetchAll(): Argument #3 must be empty when class provided in argument #2 ($class) does not have a constructor +ValueError: PDOStatement::setFetchMode(): Argument #3 must be empty when class provided in argument #2 ($class) does not have a constructor diff --git a/ext/pdo/tests/pdo_stmt_fetchobject_ctor_args.phpt b/ext/pdo/tests/pdo_stmt_fetchobject_ctor_args.phpt index cb98ebca6c3e5..db0ea90e52c55 100644 --- a/ext/pdo/tests/pdo_stmt_fetchobject_ctor_args.phpt +++ b/ext/pdo/tests/pdo_stmt_fetchobject_ctor_args.phpt @@ -40,15 +40,15 @@ class Bar { $stmt->execute(); try { $obj = $stmt->fetchObject(Foo::class); -} catch (ArgumentCountError $exception) { - echo $exception->getMessage() . "\n"; +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; } $stmt->execute(); try { $obj = $stmt->fetchObject(Foo::class, []); -} catch (ArgumentCountError $exception) { - echo $exception->getMessage() . "\n"; +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; } $stmt->execute(); @@ -66,8 +66,8 @@ var_dump($obj); try { $stmt->execute(); $obj = $stmt->fetchObject(Bar::class, ["a" => 123]); -} catch (Error $exception) { - echo $exception->getMessage() . "\n"; +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; } ?> @@ -78,8 +78,8 @@ $db = PDOTest::factory(); PDOTest::dropTableIfExists($db, "pdo_stmt_fetchobject_ctor_args"); ?> --EXPECTF-- -Too few arguments to function Foo::__construct(), 0 passed and exactly 1 expected -Too few arguments to function Foo::__construct(), 0 passed and exactly 1 expected +ArgumentCountError: Too few arguments to function Foo::__construct(), 0 passed and exactly 1 expected +ArgumentCountError: Too few arguments to function Foo::__construct(), 0 passed and exactly 1 expected object(Foo)#%d (2) { ["a"]=> int(123) @@ -94,4 +94,4 @@ object(Bar)#%d (1) { ["id"]=> int(1) } -User-supplied statement does not accept constructor arguments +ValueError: PDOStatement::fetchObject(): Argument #2 ($constructorArgs) must be empty when class provided in argument #1 ($class) does not have a constructor diff --git a/ext/pdo/tests/pdo_stmt_fetchobject_ctor_args_after_default.phpt b/ext/pdo/tests/pdo_stmt_fetchobject_ctor_args_after_default.phpt new file mode 100644 index 0000000000000..e212e968855bd --- /dev/null +++ b/ext/pdo/tests/pdo_stmt_fetchobject_ctor_args_after_default.phpt @@ -0,0 +1,68 @@ +--TEST-- +PDO Common: PDOStatement->fetchObject() with $constructorArgs when default CTORs have been set-up +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +exec("CREATE TABLE {$table} (id INT, label CHAR(1), PRIMARY KEY(id))"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (1, 'a')"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (2, 'b')"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (3, 'c')"); + +$query = "SELECT id FROM {$table} ORDER BY id ASC"; +$stmt = $db->prepare($query); +$stmt->setFetchMode(PDO::FETCH_CLASS, Foo::class, ['Hello']); +$stmt->execute(); + +var_dump($stmt->fetch()); +try { + $obj = $stmt->fetchObject(Bar::class, ['no-args']); + var_dump($obj); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} +var_dump($stmt->fetch()); + + +?> +--CLEAN-- + +--EXPECTF-- +object(Foo)#%d (2) { + ["id"]=> + int(1) + ["v"]=> + string(5) "Hello" +} +ValueError: PDOStatement::fetchObject(): Argument #2 ($constructorArgs) must be empty when class provided in argument #1 ($class) does not have a constructor +object(Foo)#%d (2) { + ["id"]=> + int(2) + ["v"]=> + string(5) "Hello" +}