diff --git a/NEWS b/NEWS index 1ed1362f3557..d9f1281d594f 100644 --- a/NEWS +++ b/NEWS @@ -1,12 +1,22 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.6.0beta1 +?? ??? ????, PHP 8.6.0beta2 + +- PDO_PGSQL: + . Fixed several lazy fetch (PDO::ATTR_PREFETCH => 0) defects: an infinite + loop when cleaning up a fetch left in a COPY, a use-after-free when a + statement with emulated or disabled prepares is destroyed, a connection + left busy for the next fetch, and rows delivered from a result another + statement took over. (KentarouTakeda) + +13 Aug 2026, PHP 8.6.0beta1 - Core: . Deprecated "namespace" as a class constant name. (NickSdot) . Changed run-tests.php to run in parallel by default, using up to 10 automatically detected workers. Pass -j1 for sequential execution. (NickSdot) + . Allowed readonly properties to declare default values. (NickSdot) . Changed run-tests.php to run test subprocesses without a shell where possible. (NickSdot) . Fixed GH-23083 (SEGV build_trace_args in zend_exceptions.c with diff --git a/UPGRADING b/UPGRADING index 779c95ae9530..f82aad3f8b3c 100644 --- a/UPGRADING +++ b/UPGRADING @@ -299,6 +299,8 @@ PHP 8.6 UPGRADE NOTES ======================================== - Core: + . Readonly properties may now declare default values. + RFC: https://wiki.php.net/rfc/readonly_property_defaults . It is now possible to use reference assign on WeakMap without the key needing to be present beforehand. . It is now possible to define the __debugInfo() magic method on enums. diff --git a/Zend/tests/readonly_classes/readonly_with_property_default.phpt b/Zend/tests/readonly_classes/readonly_with_property_default.phpt new file mode 100644 index 000000000000..49d2bdf03a1e --- /dev/null +++ b/Zend/tests/readonly_classes/readonly_with_property_default.phpt @@ -0,0 +1,36 @@ +--TEST-- +Properties of a readonly class may have default values +--FILE-- +bar = 2; + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +} + +$foo = new Foo(); +var_dump($foo->bar); +var_dump($foo->nullable); + +try { + $foo->bar = 3; +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +Error: Cannot modify readonly property Foo::$bar +int(1) +NULL +Error: Cannot modify readonly property Foo::$bar diff --git a/Zend/tests/readonly_classes/readonly_with_property_default_trait.phpt b/Zend/tests/readonly_classes/readonly_with_property_default_trait.phpt new file mode 100644 index 000000000000..9decd22a8baf --- /dev/null +++ b/Zend/tests/readonly_classes/readonly_with_property_default_trait.phpt @@ -0,0 +1,25 @@ +--TEST-- +Readonly class may use readonly trait property with default value +--FILE-- +prop); + +class B { + use TDefault; +} + +var_dump(new B()->prop); + +?> +--EXPECT-- +int(2) +int(2) diff --git a/Zend/tests/readonly_props/readonly_clone_success1.phpt b/Zend/tests/readonly_props/readonly_clone_success1.phpt index 72cd9e9622b3..50e5e6df81e2 100644 --- a/Zend/tests/readonly_props/readonly_clone_success1.phpt +++ b/Zend/tests/readonly_props/readonly_clone_success1.phpt @@ -23,6 +23,41 @@ var_dump($foo2); var_dump(clone $foo2); +class FooWithDefault { + public readonly int $bar = 1; + + public function __clone() + { + $this->bar++; + } +} + +$fooWithDefault = new FooWithDefault(); + +var_dump(clone $fooWithDefault); + +$fooWithDefault2 = clone $fooWithDefault; +var_dump($fooWithDefault2); + +var_dump(clone $fooWithDefault2); + +class FooWithDefaultCloneWith { + public readonly int $bar = 1; + + public function withBar(int $bar) + { + return clone($this, ['bar' => $bar]); + } +} + +$clone = new FooWithDefaultCloneWith(); +var_dump($clone); + +$clone2 = $clone->withBar(2); +var_dump($clone2); + +var_dump($clone2->withBar(0)); + ?> --EXPECTF-- object(Foo)#%d (%d) { @@ -37,3 +72,27 @@ object(Foo)#%d (%d) { ["bar"]=> int(3) } +object(FooWithDefault)#%d (%d) { + ["bar"]=> + int(2) +} +object(FooWithDefault)#%d (%d) { + ["bar"]=> + int(2) +} +object(FooWithDefault)#%d (%d) { + ["bar"]=> + int(3) +} +object(FooWithDefaultCloneWith)#%d (%d) { + ["bar"]=> + int(1) +} +object(FooWithDefaultCloneWith)#%d (%d) { + ["bar"]=> + int(2) +} +object(FooWithDefaultCloneWith)#%d (%d) { + ["bar"]=> + int(0) +} diff --git a/Zend/tests/readonly_props/readonly_trait_match.phpt b/Zend/tests/readonly_props/readonly_trait_match.phpt index 00aa6349aa14..36f2e11ec2bc 100644 --- a/Zend/tests/readonly_props/readonly_trait_match.phpt +++ b/Zend/tests/readonly_props/readonly_trait_match.phpt @@ -13,7 +13,20 @@ class C { use T1, T2; } +trait TDefault1 { + public readonly int $prop = 1; +} +trait TDefault2 { + public readonly int $prop = 1; +} +class CDefault { + use TDefault1, TDefault2; +} + +var_dump(new CDefault()->prop); + ?> ===DONE=== --EXPECT-- +int(1) ===DONE=== diff --git a/Zend/tests/readonly_props/readonly_with_default.phpt b/Zend/tests/readonly_props/readonly_with_default.phpt index 12afe5cde153..affe62f8d1ff 100644 --- a/Zend/tests/readonly_props/readonly_with_default.phpt +++ b/Zend/tests/readonly_props/readonly_with_default.phpt @@ -3,17 +3,43 @@ Readonly property with default value --FILE-- 2]; + public readonly E $enum = E::Case; + public readonly string $enumString = E::Case->name; } $test = new Test; +var_dump($test->prop); +var_dump($test->className); +var_dump($test->nullable); +var_dump($test->array); +var_dump($test->enum); +var_dump($test->enumString); try { $test->prop = 2; } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } ?> ---EXPECTF-- -Fatal error: Readonly property Test::$prop cannot have default value in %s on line %d +--EXPECT-- +int(1) +string(4) "Test" +NULL +array(2) { + [0]=> + int(1) + ["two"]=> + int(2) +} +enum(E::Case) +string(4) "Case" +Error: Cannot modify readonly property Test::$prop diff --git a/Zend/tests/readonly_props/readonly_with_default_abstract_get_set_implicit_set.phpt b/Zend/tests/readonly_props/readonly_with_default_abstract_get_set_implicit_set.phpt new file mode 100644 index 000000000000..e61574d07aaf --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_abstract_get_set_implicit_set.phpt @@ -0,0 +1,18 @@ +--TEST-- +Readonly property with default value has restricted set visibility for get/set abstract property +--DESCRIPTION-- +The error message should be improved, the set access level comes from readonly. +--FILE-- + +--EXPECTF-- +Fatal error: Set access level of C::$prop must be omitted (as in class P) in %s on line %d diff --git a/Zend/tests/readonly_props/readonly_with_default_asymmetric_visibility.phpt b/Zend/tests/readonly_props/readonly_with_default_asymmetric_visibility.phpt new file mode 100644 index 000000000000..11b5a741ca17 --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_asymmetric_visibility.phpt @@ -0,0 +1,34 @@ +--TEST-- +Readonly property with default value and asymmetric visibility +--FILE-- +$prop; + try { + $test->$prop = 42; + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + echo "$$prop before $before, after {$test->$prop}", PHP_EOL; +} + +?> +--EXPECT-- +Error: Cannot modify readonly property Test::$default +$default before 1, after 1 +Error: Cannot modify readonly property Test::$private +$private before 2, after 2 +Error: Cannot modify readonly property Test::$protected +$protected before 3, after 3 +Error: Cannot modify readonly property Test::$public +$public before 4, after 4 diff --git a/Zend/tests/readonly_props/readonly_with_default_inheritance.phpt b/Zend/tests/readonly_props/readonly_with_default_inheritance.phpt new file mode 100644 index 000000000000..8b285dcdd769 --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_inheritance.phpt @@ -0,0 +1,40 @@ +--TEST-- +Readonly property with default value and inheritance +--FILE-- +prop; + } +} + +class PrivateChild extends PrivateParent { + public readonly int $prop = 4; +} + +var_dump(new ChildInherits()->prop); +var_dump(new ChildOverrides()->prop); + +$privateChild = new PrivateChild(); +var_dump($privateChild->getParentProp()); +var_dump($privateChild->prop); + +?> +--EXPECT-- +int(1) +int(2) +int(3) +int(4) diff --git a/Zend/tests/readonly_props/readonly_with_default_interface_get_only.phpt b/Zend/tests/readonly_props/readonly_with_default_interface_get_only.phpt new file mode 100644 index 000000000000..1e64e26b4517 --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_interface_get_only.phpt @@ -0,0 +1,17 @@ +--TEST-- +Readonly property with default value satisfies get-only interface property +--FILE-- +prop); +?> +--EXPECT-- +int(42) diff --git a/Zend/tests/readonly_props/readonly_with_default_interface_get_set.phpt b/Zend/tests/readonly_props/readonly_with_default_interface_get_set.phpt new file mode 100644 index 000000000000..102444cc044f --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_interface_get_set.phpt @@ -0,0 +1,16 @@ +--TEST-- +Readonly public(set) property with default value does not satisfy get/set interface property +--FILE-- + +--EXPECTF-- +Fatal error: Class C contains 1 abstract method and must therefore be declared abstract or implement the remaining method (I::$prop::set) in %s on line %d diff --git a/Zend/tests/readonly_props/readonly_with_default_interface_get_set_implicit_set.phpt b/Zend/tests/readonly_props/readonly_with_default_interface_get_set_implicit_set.phpt new file mode 100644 index 000000000000..106bf2aeb0cf --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_interface_get_set_implicit_set.phpt @@ -0,0 +1,17 @@ +--TEST-- +Readonly property with default value has restricted set visibility for get/set interface property +--DESCRIPTION-- +The error message should be improved, the set access level comes from readonly. Ref: Zend/tests/property_hooks/interface_get_set_readonly.phpt +--FILE-- + +--EXPECTF-- +Fatal error: Set access level of C::$prop must be omitted (as in class I) in %s on line %d diff --git a/Zend/tests/readonly_props/readonly_with_default_trait_mismatch.phpt b/Zend/tests/readonly_props/readonly_with_default_trait_mismatch.phpt new file mode 100644 index 000000000000..6668b1d860bb --- /dev/null +++ b/Zend/tests/readonly_props/readonly_with_default_trait_mismatch.phpt @@ -0,0 +1,20 @@ +--TEST-- +Readonly trait property default value mismatch +--FILE-- + +--EXPECTF-- +Fatal error: T1 and T2 define the same property ($prop) in the composition of C. However, the definition differs and is considered incompatible. Class was composed in %s on line %d diff --git a/Zend/tests/readonly_props/serialization.phpt b/Zend/tests/readonly_props/serialization.phpt index f9e1f364673f..0d56067760d5 100644 --- a/Zend/tests/readonly_props/serialization.phpt +++ b/Zend/tests/readonly_props/serialization.phpt @@ -17,6 +17,42 @@ var_dump(unserialize($s)); var_dump(unserialize("O:4:\"Test\":1:{s:4:\"prop\";i:2;}")); var_dump(unserialize("O:4:\"Test\":2:{s:4:\"prop\";i:2;s:4:\"prop\";i:3;}")); +class TestDefault { + public readonly int $prop = 1; +} + +var_dump($s = serialize(new TestDefault)); +var_dump(unserialize($s)); + +var_dump(unserialize("O:11:\"TestDefault\":0:{}")); +var_dump(unserialize("O:11:\"TestDefault\":1:{s:4:\"prop\";i:2;}")); + +class TestDefaultWithUnserialize { + public readonly int $prop = 1; + public public(set) readonly int $lock = 1; + + public function __unserialize(array $data): void { + foreach ($data as $key => $value) { + $this->{$key} = $value; + } + } +} + +$testDefaultWithUnserialize = unserialize("O:26:\"TestDefaultWithUnserialize\":1:{s:4:\"prop\";i:2;}"); +var_dump($testDefaultWithUnserialize); + +try { + $testDefaultWithUnserialize->prop = 3; +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +try { + $testDefaultWithUnserialize->lock = 3; +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + ?> --EXPECT-- string(30) "O:4:"Test":1:{s:4:"prop";i:1;}" @@ -32,3 +68,24 @@ object(Test)#1 (1) { ["prop"]=> int(3) } +string(38) "O:11:"TestDefault":1:{s:4:"prop";i:1;}" +object(TestDefault)#1 (1) { + ["prop"]=> + int(1) +} +object(TestDefault)#1 (1) { + ["prop"]=> + int(1) +} +object(TestDefault)#1 (1) { + ["prop"]=> + int(2) +} +object(TestDefaultWithUnserialize)#1 (2) { + ["prop"]=> + int(2) + ["lock"]=> + int(1) +} +Error: Cannot modify readonly property TestDefaultWithUnserialize::$prop +Error: Cannot modify readonly property TestDefaultWithUnserialize::$lock diff --git a/Zend/tests/readonly_props/unset.phpt b/Zend/tests/readonly_props/unset.phpt index b8bd4218fa0c..823c021f6122 100644 --- a/Zend/tests/readonly_props/unset.phpt +++ b/Zend/tests/readonly_props/unset.phpt @@ -54,6 +54,31 @@ try { echo $e->getMessage(), "\n"; } +class Test4 { + public readonly int $prop = 1; + + public function __construct() { + try { + unset($this->prop); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } + + public function __get($name) { + throw new Exception('Unreachable'); + } +} + +$test = new Test4; +var_dump($test->prop); // Don't call __get. +try { + unset($test->prop); +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +var_dump($test->prop); // Still don't call __get. + ?> --EXPECT-- Cannot unset readonly property Test::$prop @@ -62,3 +87,7 @@ int(1) int(1) Cannot unset readonly property Test2::$prop Cannot unset protected(set) readonly property Test3::$prop from global scope +Error: Cannot unset readonly property Test4::$prop +int(1) +Error: Cannot unset readonly property Test4::$prop +int(1) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 6fd4df023520..1d837c59832b 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -9497,11 +9497,6 @@ static void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t f zend_error_noreturn(E_COMPILE_ERROR, "Readonly property %s::$%s must have type", ZSTR_VAL(ce->name), ZSTR_VAL(name)); } - if (!Z_ISUNDEF(value_zv)) { - zend_error_noreturn(E_COMPILE_ERROR, - "Readonly property %s::$%s cannot have default value", - ZSTR_VAL(ce->name), ZSTR_VAL(name)); - } if (flags & ZEND_ACC_STATIC) { zend_error_noreturn(E_COMPILE_ERROR, "Static property %s::$%s cannot be readonly", diff --git a/ext/pdo_pgsql/pgsql_statement.c b/ext/pdo_pgsql/pgsql_statement.c index 1620544556b6..4deba242bbb6 100644 --- a/ext/pdo_pgsql/pgsql_statement.c +++ b/ext/pdo_pgsql/pgsql_statement.c @@ -64,12 +64,12 @@ static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode) { pdo_pgsql_db_handle *H = S->H; - if (S->is_running_unbuffered && S->result && (fin_mode & FIN_ABORT)) { + /* a buffered query may have already drained this statement's stream */ + if (S->is_running_unbuffered && H->running_stmt == S && S->result && (fin_mode & FIN_ABORT)) { PGcancel *cancel = PQgetCancel(H->server); char errbuf[256]; PQcancel(cancel, errbuf, 256); PQfreeCancel(cancel); - S->is_running_unbuffered = false; } if (S->result) { @@ -78,7 +78,7 @@ static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode) S->result = NULL; } - if (S->is_running_unbuffered) { + if (S->is_running_unbuffered && H->running_stmt == S) { /* https://postgresql.org/docs/current/libpq-async.html: * "PQsendQuery cannot be called again until PQgetResult has returned NULL" * And as all single-row functions are connection-wise instead of statement-wise, @@ -88,8 +88,35 @@ static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode) // instead of discarding results we could store them to their statement // so that their fetch() will get them (albeit not in lazy mode anymore). while ((S->result = PQgetResult(H->server))) { + ExecStatusType status = PQresultStatus(S->result); + PQclear(S->result); S->result = NULL; + + /* PQgetResult() keeps handing out the same result while the + * connection is copying: only these calls can end it */ + if (status == PGRES_COPY_IN || status == PGRES_COPY_BOTH) { + /* fail a copy in, so that abandoning a statement cannot + * commit it; a replication stream only accepts a clean end */ + const char *error = status == PGRES_COPY_IN + ? "COPY terminated by PDO" + : NULL; + + if (PQputCopyEnd(H->server, error) < 0) { + break; + } + } + if (status == PGRES_COPY_OUT || status == PGRES_COPY_BOTH) { + char *buf; + int nbytes; + + while ((nbytes = PQgetCopyData(H->server, &buf, 0)) > 0) { + PQfreemem(buf); + } + if (nbytes < -1) { + break; + } + } } S->is_running_unbuffered = false; } @@ -111,9 +138,6 @@ static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode) } S->is_prepared = false; - if (H->running_stmt == S) { - H->running_stmt = NULL; - } } } @@ -124,6 +148,10 @@ static int pgsql_stmt_dtor(pdo_stmt_t *stmt) pgsql_stmt_finish(S, FIN_DISCARD|(server_obj_usable ? FIN_CLOSE|FIN_ABORT : 0)); + if (server_obj_usable && S->H->running_stmt == S) { + S->H->running_stmt = NULL; + } + if (S->stmt_name) { efree(S->stmt_name); S->stmt_name = NULL; @@ -559,7 +587,7 @@ static int pgsql_stmt_fetch(pdo_stmt_t *stmt, return 0; } } else { - if (S->is_running_unbuffered && S->current_row >= stmt->row_count) { + if (S->is_running_unbuffered && S->H->running_stmt == S && S->current_row >= stmt->row_count) { ExecStatusType status; /* @todo in unbuffered mode, PQ allows multiple queries to be passed: @@ -588,12 +616,12 @@ static int pgsql_stmt_fetch(pdo_stmt_t *stmt, S->current_row = 0; if (!stmt->row_count) { - S->is_running_unbuffered = false; /* libpq requires looping until getResult returns null */ pgsql_stmt_finish(S, 0); } } - if (S->current_row < stmt->row_count) { + /* another statement may have taken over and freed the result */ + if (S->result && S->current_row < stmt->row_count) { S->current_row++; return 1; } else { diff --git a/ext/pdo_pgsql/tests/lazy_fetch_cancel.phpt b/ext/pdo_pgsql/tests/lazy_fetch_cancel.phpt new file mode 100644 index 000000000000..7968c2653206 --- /dev/null +++ b/ext/pdo_pgsql/tests/lazy_fetch_cancel.phpt @@ -0,0 +1,36 @@ +--TEST-- +PDO PgSQL an abandoned lazy fetch frees the connection without a prepared statement +--EXTENSIONS-- +pdo_pgsql +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +foreach ([ + 'PDO::ATTR_EMULATE_PREPARES' => [PDO::ATTR_EMULATE_PREPARES => true], + 'Pdo\Pgsql::ATTR_DISABLE_PREPARES' => [Pdo\Pgsql::ATTR_DISABLE_PREPARES => true], +] as $label => $options) { + $options[PDO::ATTR_PREFETCH] = 0; + + $stmt = $pdo->prepare("VALUES (1), (2)", $options); + $stmt->execute(); + $stmt = null; + + $stmt = $pdo->prepare("VALUES (1), (2)", $options); + $stmt->execute(); + echo "$label: "; + var_dump((bool) $stmt->fetchAll()); +} +?> +--EXPECT-- +PDO::ATTR_EMULATE_PREPARES: bool(true) +Pdo\Pgsql::ATTR_DISABLE_PREPARES: bool(true) diff --git a/ext/pdo_pgsql/tests/lazy_fetch_copy.phpt b/ext/pdo_pgsql/tests/lazy_fetch_copy.phpt new file mode 100644 index 000000000000..91321e2bcde2 --- /dev/null +++ b/ext/pdo_pgsql/tests/lazy_fetch_copy.phpt @@ -0,0 +1,35 @@ +--TEST-- +PDO PgSQL a lazy fetch left in a COPY does not hang the connection cleanup +--EXTENSIONS-- +pdo_pgsql +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); +$pdo->setAttribute(PDO::ATTR_PREFETCH, 0); +$pdo->exec("CREATE TEMPORARY TABLE lazy_fetch_copy (i int)"); + +foreach ([ + 'COPY OUT' => "COPY (SELECT 1) TO STDOUT", + 'COPY IN' => "COPY lazy_fetch_copy FROM STDIN", +] as $label => $sql) { + $copy = $pdo->prepare($sql); + $copy->execute(); + + $stmt = $pdo->prepare("VALUES (1), (2)"); + $stmt->execute(); + echo "$label: "; + var_dump((bool) $stmt->fetchAll()); +} +?> +--EXPECT-- +COPY OUT: bool(true) +COPY IN: bool(true) diff --git a/ext/pdo_pgsql/tests/lazy_fetch_drain.phpt b/ext/pdo_pgsql/tests/lazy_fetch_drain.phpt new file mode 100644 index 000000000000..a650628d3cce --- /dev/null +++ b/ext/pdo_pgsql/tests/lazy_fetch_drain.phpt @@ -0,0 +1,36 @@ +--TEST-- +PDO PgSQL a drained lazy fetch frees the connection without a prepared statement +--EXTENSIONS-- +pdo_pgsql +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +foreach ([ + 'PDO::ATTR_EMULATE_PREPARES' => [PDO::ATTR_EMULATE_PREPARES => true], + 'Pdo\Pgsql::ATTR_DISABLE_PREPARES' => [Pdo\Pgsql::ATTR_DISABLE_PREPARES => true], +] as $label => $options) { + $options[PDO::ATTR_PREFETCH] = 0; + + $stmt = $pdo->prepare("VALUES (1), (2)", $options); + $stmt->execute(); + $stmt->fetchAll(); + + $stmt = $pdo->prepare("VALUES (1), (2)", $options); + $stmt->execute(); + echo "$label: "; + var_dump((bool) $stmt->fetchAll()); +} +?> +--EXPECT-- +PDO::ATTR_EMULATE_PREPARES: bool(true) +Pdo\Pgsql::ATTR_DISABLE_PREPARES: bool(true) diff --git a/ext/pdo_pgsql/tests/lazy_fetch_takeover.phpt b/ext/pdo_pgsql/tests/lazy_fetch_takeover.phpt new file mode 100644 index 000000000000..eace678310de --- /dev/null +++ b/ext/pdo_pgsql/tests/lazy_fetch_takeover.phpt @@ -0,0 +1,28 @@ +--TEST-- +PDO PgSQL a lazy fetch whose stream was taken over reports no leftover rows +--EXTENSIONS-- +pdo_pgsql +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pdo->setAttribute(PDO::ATTR_PREFETCH, 0); + +$first = $pdo->prepare("VALUES (1), (2)"); +$first->execute(); + +$pdo->prepare("VALUES (1), (2)")->execute(); + +var_dump($first->fetchAll(PDO::FETCH_NUM)); +?> +--EXPECT-- +array(0) { +} diff --git a/ext/pdo_pgsql/tests/lazy_fetch_takeover_buffered.phpt b/ext/pdo_pgsql/tests/lazy_fetch_takeover_buffered.phpt new file mode 100644 index 000000000000..aa8c2aa86c4a --- /dev/null +++ b/ext/pdo_pgsql/tests/lazy_fetch_takeover_buffered.phpt @@ -0,0 +1,38 @@ +--TEST-- +PDO PgSQL a lazy fetch stale after a buffered query does not read the next statement's rows +--EXTENSIONS-- +pdo_pgsql +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +$first = $pdo->prepare("VALUES (1), (2), (3)", [PDO::ATTR_PREFETCH => 0]); +$first->execute(); +$first->fetch(); + +// a buffered query drains the stream but does not end $first's lazy fetch +$pdo->prepare("VALUES (99)")->execute(); + +$third = $pdo->prepare("VALUES (777), (888)", [PDO::ATTR_PREFETCH => 0]); +$third->execute(); + +var_dump($first->fetch(PDO::FETCH_NUM)); +var_dump($third->fetchAll(PDO::FETCH_COLUMN)); +?> +--EXPECT-- +bool(false) +array(2) { + [0]=> + string(3) "777" + [1]=> + string(3) "888" +} diff --git a/ext/reflection/tests/ReflectionClass_toString_009.phpt b/ext/reflection/tests/ReflectionClass_toString_009.phpt new file mode 100644 index 000000000000..b2d118c459b8 --- /dev/null +++ b/ext/reflection/tests/ReflectionClass_toString_009.phpt @@ -0,0 +1,32 @@ +--TEST-- +ReflectionClass::__toString() - readonly property with default +--FILE-- + +--EXPECTF-- +Class [ class Test ] { + @@ %s 3-5 + + - Constants [0] { + } + + - Static properties [0] { + } + + - Static methods [0] { + } + + - Properties [1] { + Property [ public protected(set) readonly int $property = 42 ] + } + + - Methods [0] { + } +} diff --git a/ext/reflection/tests/ReflectionProperty_readonly_with_default.phpt b/ext/reflection/tests/ReflectionProperty_readonly_with_default.phpt new file mode 100644 index 000000000000..d3c3e8a084e4 --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_readonly_with_default.phpt @@ -0,0 +1,35 @@ +--TEST-- +Reflection for readonly property with default value +--FILE-- +isReadOnly()); +var_dump($rp->hasDefaultValue()); +var_dump($rp->getDefaultValue()); +var_dump(new ReflectionClass(Foo::class)->getDefaultProperties()); + +$test = new Foo(); +try { + $rp->setValue($test, 2); +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +bool(true) +bool(true) +int(1) +array(2) { + ["prop"]=> + int(1) + ["nullable"]=> + NULL +} +Error: Cannot modify readonly property Foo::$prop diff --git a/ext/reflection/tests/ReflectionProperty_toString_002.phpt b/ext/reflection/tests/ReflectionProperty_toString_002.phpt new file mode 100644 index 000000000000..5b93d2149b9a --- /dev/null +++ b/ext/reflection/tests/ReflectionProperty_toString_002.phpt @@ -0,0 +1,14 @@ +--TEST-- +ReflectionProperty::__toString() - readonly with default +--FILE-- + +--EXPECT-- +Property [ public protected(set) readonly int $nick = 42 ] diff --git a/ext/spl/spl_observer.c b/ext/spl/spl_observer.c index adfc172af982..a980828cdcb2 100644 --- a/ext/spl/spl_observer.c +++ b/ext/spl/spl_observer.c @@ -747,7 +747,11 @@ PHP_METHOD(SplObjectStorage, count) } if (mode == PHP_COUNT_RECURSIVE) { - RETURN_LONG(php_count_recursive(&intern->storage)); + zend_long count = php_count_recursive(&intern->storage); + if (UNEXPECTED(count < 0)) { + RETURN_THROWS(); + } + RETURN_LONG(count); } RETURN_LONG(zend_hash_num_elements(&intern->storage)); diff --git a/ext/standard/array.c b/ext/standard/array.c index 91d3932516eb..648b48479099 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -560,10 +560,18 @@ PHPAPI zend_long php_count_recursive(HashTable *ht) /* {{{ */ zend_long cnt = 0; zval *element; +#ifdef ZEND_CHECK_STACK_LIMIT + if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { + zend_call_stack_size_error(); + return -1; + } +#endif + if (!(GC_FLAGS(ht) & GC_IMMUTABLE)) { if (GC_IS_RECURSIVE(ht)) { php_error_docref(NULL, E_WARNING, "Recursion detected"); - return 0; + /* A user error handler may have thrown. */ + return EG(exception) ? -1 : 0; } GC_PROTECT_RECURSION(ht); } @@ -572,7 +580,12 @@ PHPAPI zend_long php_count_recursive(HashTable *ht) /* {{{ */ ZEND_HASH_FOREACH_VAL(ht, element) { ZVAL_DEREF(element); if (Z_TYPE_P(element) == IS_ARRAY) { - cnt += php_count_recursive(Z_ARRVAL_P(element)); + zend_long sub_cnt = php_count_recursive(Z_ARRVAL_P(element)); + if (UNEXPECTED(sub_cnt < 0)) { + cnt = -1; + break; + } + cnt += sub_cnt; } } ZEND_HASH_FOREACH_END(); @@ -617,6 +630,9 @@ PHP_FUNCTION(count) cnt = zend_hash_num_elements(Z_ARRVAL_P(array)); } else { cnt = php_count_recursive(Z_ARRVAL_P(array)); + if (UNEXPECTED(cnt < 0)) { + RETURN_THROWS(); + } } RETURN_LONG(cnt); case IS_OBJECT: { diff --git a/ext/standard/filestat.c b/ext/standard/filestat.c index 96669a809346..13478b6f5c31 100644 --- a/ext/standard/filestat.c +++ b/ext/standard/filestat.c @@ -736,12 +736,12 @@ PHPAPI void php_stat(zend_string *filename, int type, zval *return_value) RETURN_FALSE; } if (IS_ACCESS_CHECK(type)) { - if ((wrapper = php_stream_locate_url_wrapper(ZSTR_VAL(filename), &local, 0)) == &php_plain_files_wrapper - && php_check_open_basedir(local)) { - RETURN_FALSE; - } - + wrapper = php_stream_locate_url_wrapper(ZSTR_VAL(filename), &local, 0); if (wrapper == &php_plain_files_wrapper) { + if (php_check_open_basedir(local)) { + RETURN_FALSE; + } + char realpath[MAXPATHLEN]; const char *file_path_to_check; /* if the wrapper is not found, we need to expand path to match open behavior */ diff --git a/ext/standard/php_array.h b/ext/standard/php_array.h index ab6c8494dd4e..a7e8afdec34b 100644 --- a/ext/standard/php_array.h +++ b/ext/standard/php_array.h @@ -27,6 +27,7 @@ PHPAPI int php_array_merge(HashTable *dest, HashTable *src); PHPAPI int php_array_merge_recursive(HashTable *dest, HashTable *src); PHPAPI int php_array_replace_recursive(HashTable *dest, HashTable *src); PHPAPI int php_multisort_compare(const void *a, const void *b); +/* Returns -1 and throws if the array is nested too deeply. */ PHPAPI zend_long php_count_recursive(HashTable *ht); PHPAPI bool php_array_data_shuffle(php_random_algo_with_state engine, zval *array); diff --git a/ext/standard/string.c b/ext/standard/string.c index d8a7479d2a98..91c930515b78 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -943,6 +943,9 @@ PHPAPI void php_implode(const zend_string *glue, HashTable *pieces, zval *return uint32_t flags = ZSTR_GET_COPYABLE_CONCAT_PROPERTIES(glue); + /* Converting an element may call __toString(), which can destroy pieces. */ + GC_TRY_ADDREF(pieces); + ZEND_HASH_FOREACH_VAL(pieces, tmp) { if (EXPECTED(Z_TYPE_P(tmp) == IS_STRING)) { ptr->str = Z_STR_P(tmp); @@ -1006,6 +1009,7 @@ PHPAPI void php_implode(const zend_string *glue, HashTable *pieces, zval *return } free_alloca(strings, use_heap); + GC_TRY_DTOR_NO_REF(pieces); RETURN_NEW_STR(str); } /* }}} */ @@ -3373,7 +3377,12 @@ static void php_strtr_array(zval *return_value, zend_string *str, HashTable *fro { if (zend_hash_num_elements(from_ht) < 1) { RETURN_STR_COPY(str); - } else if (zend_hash_num_elements(from_ht) == 1) { + } + + /* Converting a replacement may call __toString(), which can destroy from_ht. */ + GC_TRY_ADDREF(from_ht); + + if (zend_hash_num_elements(from_ht) == 1) { zend_long num_key; zend_string *str_key, *tmp_str, *replace, *tmp_replace; zval *entry; @@ -3402,11 +3411,13 @@ static void php_strtr_array(zval *return_value, zend_string *str, HashTable *fro } zend_tmp_string_release(tmp_str); zend_tmp_string_release(tmp_replace); - return; + break; } ZEND_HASH_FOREACH_END(); } else { php_strtr_array_ex(return_value, str, from_ht); } + + GC_TRY_DTOR_NO_REF(from_ht); } /* {{{ Translates characters in str using given translation tables */ @@ -4466,6 +4477,17 @@ static void _php_str_replace_common( RETURN_THROWS(); } + /* Converting an element may call __toString(), which can destroy the arrays. */ + if (search_ht) { + GC_TRY_ADDREF(search_ht); + } + if (replace_ht) { + GC_TRY_ADDREF(replace_ht); + } + if (subject_ht) { + GC_TRY_ADDREF(subject_ht); + } + /* if subject is an array */ if (subject_ht) { array_init(return_value); @@ -4492,6 +4514,16 @@ static void _php_str_replace_common( if (zcount) { ZEND_TRY_ASSIGN_REF_LONG(zcount, count); } + + if (search_ht) { + GC_TRY_DTOR_NO_REF(search_ht); + } + if (replace_ht) { + GC_TRY_DTOR_NO_REF(replace_ht); + } + if (subject_ht) { + GC_TRY_DTOR_NO_REF(subject_ht); + } } /* {{{ php_str_replace_common */ diff --git a/ext/standard/tests/array/count_recursive_stack_limit.phpt b/ext/standard/tests/array/count_recursive_stack_limit.phpt new file mode 100644 index 000000000000..a7f3918ba350 --- /dev/null +++ b/ext/standard/tests/array/count_recursive_stack_limit.phpt @@ -0,0 +1,32 @@ +--TEST-- +Stack overflow in count() with COUNT_RECURSIVE and deeply nested arrays +--SKIPIF-- + +--INI-- +zend.max_allowed_stack_size=256K +--FILE-- +getMessage(), "\n"; + var_dump($e->getPrevious()); +} +?> +--EXPECTF-- +Error: Maximum call stack size of %d bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion? +NULL diff --git a/ext/standard/tests/strings/gh23204.phpt b/ext/standard/tests/strings/gh23204.phpt new file mode 100644 index 000000000000..e2ae20592c5a --- /dev/null +++ b/ext/standard/tests/strings/gh23204.phpt @@ -0,0 +1,109 @@ +--TEST-- +GH-23204 (Use-after-free when __toString() destroys the array being read) +--CREDITS-- +e1abrador +--FILE-- +getMessage(), "\n"; +} + +class UnsetPats implements Stringable { + public function __toString(): string { + global $d; + $d = null; + return "X"; + } +} + +$d = ["aa" => new UnsetPats, "bb" => "2", "cc" => "3", "dd" => "4"]; +echo "strtr: ", strtr("aabbccdd", $d), "\n"; + +$e = ["aa" => new UnsetPats]; +$d = &$e; +echo "strtr single: ", strtr("aabb", $e), "\n"; + +class UnsetSearch implements Stringable { + public function __toString(): string { + global $f; + $f = null; + return "a"; + } +} + +$f = [new UnsetSearch, "b", "c", "d"]; +echo "str_replace search: ", str_replace($f, "z", "abcd"), "\n"; + +class UnsetReplace implements Stringable { + public function __toString(): string { + global $g; + $g = null; + return "z"; + } +} + +$g = [new UnsetReplace, "y", "y", "y"]; +echo "str_replace replace: ", str_replace(["a", "b", "c", "d"], $g, "abcd"), "\n"; + +class UnsetSubject implements Stringable { + public function __toString(): string { + global $h; + $h = null; + return "abcd"; + } +} + +$h = [new UnsetSubject, "abcd"]; +var_dump(str_replace("a", "z", $h)); +?> +--EXPECT-- +destroyed: X,2,3,4 +NULL +appended: X,2,3,4 +count: 5 +Exception: boom +strtr: X234 +strtr single: Xbb +str_replace search: zzzz +str_replace replace: zyyy +array(2) { + [0]=> + string(4) "zbcd" + [1]=> + string(4) "zbcd" +} diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re index eca9660c5605..1fb8793c2bbc 100644 --- a/ext/standard/var_unserializer.re +++ b/ext/standard/var_unserializer.re @@ -17,6 +17,7 @@ #include "php_incomplete_class.h" #include "zend_portability.h" #include "zend_exceptions.h" +#include "zend_objects.h" /* {{{ reference-handling for unserializer: var_* */ #define VAR_ENTRIES_MAX 1018 /* 1024 - offsetof(php_unserialize_data, entries) / sizeof(void*) */ @@ -300,6 +301,7 @@ PHPAPI void var_destroy(php_unserialize_data_t *var_hashx) zval param; ZVAL_COPY(¶m, &var_dtor_hash->data[i + 1]); + zend_object_set_properties_reinitable(Z_OBJ_P(zv), /* reinitable */ true); BG(serialize_lock)++; zend_call_known_instance_method_with_1_params( Z_OBJCE_P(zv)->__unserialize, Z_OBJ_P(zv), NULL, ¶m); @@ -308,6 +310,7 @@ PHPAPI void var_destroy(php_unserialize_data_t *var_hashx) GC_ADD_FLAGS(Z_OBJ_P(zv), IS_OBJ_DESTRUCTOR_CALLED); } BG(serialize_lock)--; + zend_object_set_properties_reinitable(Z_OBJ_P(zv), /* reinitable */ false); zval_ptr_dtor(¶m); } else { GC_ADD_FLAGS(Z_OBJ_P(zv), IS_OBJ_DESTRUCTOR_CALLED);