From 60d8e92ed4a0c1b310a81103f3b423713aece6c8 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 25 Jun 2026 13:35:08 -0400 Subject: [PATCH 1/2] Guard uninitialized SplFileObject in fputcsv() and next() fputcsv() and next() reached the file stream without the CHECK_SPL_FILE_OBJECT_IS_INITIALIZED guard their siblings carry, so invoking them via reflection on an object from newInstanceWithoutConstructor() (constructor bypassed, stream NULL) crashed instead of throwing. next() only touches the stream with the READ_AHEAD flag, itself settable through the equally unguarded setFlags(). Both now throw "Object not initialized". Fixes GH-16217 Closes GH-22454 --- NEWS | 2 ++ ext/spl/spl_directory.c | 4 ++++ ext/spl/tests/gh16217.phpt | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 ext/spl/tests/gh16217.phpt diff --git a/NEWS b/NEWS index 7dd1aea4b881..39f2359152f0 100644 --- a/NEWS +++ b/NEWS @@ -24,6 +24,8 @@ PHP NEWS (jorgsowa) . Ignore leading back-slash in class_parents(), class_implements(), and class_uses(). (jorgsowa) + . Fixed bug GH-16217 (SplFileObject::fputcsv() on an uninitialized object + segfaults). (iliaal) - Standard: . Fixed bug GH-22360 (convert.base64-encode corruption on diff --git a/ext/spl/spl_directory.c b/ext/spl/spl_directory.c index f83ea9f84aca..eb3fb8caef64 100644 --- a/ext/spl/spl_directory.c +++ b/ext/spl/spl_directory.c @@ -2224,6 +2224,8 @@ PHP_METHOD(SplFileObject, next) RETURN_THROWS(); } + CHECK_SPL_FILE_OBJECT_IS_INITIALIZED(intern); + spl_filesystem_file_free_line(intern); if (SPL_HAS_FLAG(intern->flags, SPL_FILE_OBJECT_READ_AHEAD)) { spl_filesystem_file_read_line(ZEND_THIS, intern, true); @@ -2376,6 +2378,8 @@ PHP_METHOD(SplFileObject, fputcsv) RETURN_THROWS(); } + CHECK_SPL_FILE_OBJECT_IS_INITIALIZED(intern); + if (delim) { if (d_len != 1) { zend_argument_value_error(2, "must be a single character"); diff --git a/ext/spl/tests/gh16217.phpt b/ext/spl/tests/gh16217.phpt new file mode 100644 index 000000000000..71760389c8e4 --- /dev/null +++ b/ext/spl/tests/gh16217.phpt @@ -0,0 +1,35 @@ +--TEST-- +GH-16217 (SplFileObject methods on an uninitialized object segfault) +--FILE-- +newInstanceWithoutConstructor(); +} + +try { + (new ReflectionMethod(SplFileObject::class, "fputcsv"))->invoke(uninitialized(), []); +} catch (Error $e) { + echo "fputcsv: ", $e->getMessage(), "\n"; +} + +try { + (new ReflectionMethod(SplFileObject::class, "next"))->invoke(uninitialized()); +} catch (Error $e) { + echo "next: ", $e->getMessage(), "\n"; +} + +$obj = uninitialized(); +(new ReflectionMethod(SplFileObject::class, "setFlags"))->invoke($obj, SplFileObject::READ_AHEAD); +try { + (new ReflectionMethod(SplFileObject::class, "next"))->invoke($obj); +} catch (Error $e) { + echo "next (READ_AHEAD): ", $e->getMessage(), "\n"; +} + +echo "Done\n"; +?> +--EXPECT-- +fputcsv: Object not initialized +next: Object not initialized +next (READ_AHEAD): Object not initialized +Done From 86d78cc24f174aa0b5545df7c1336be00da1921e Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Wed, 24 Jun 2026 20:01:08 -0400 Subject: [PATCH 2/2] Report dynamic property shadowing a private parent in Reflection ReflectionClass::hasProperty() and getProperty() look the name up in the class's properties_info table and, on a match, return before checking the object's dynamic properties. A private property declared on a parent class lives in that table but isn't visible to the child, so a dynamic property of the same name went unreported. Fold the accessibility check into the lookup so an inaccessible private-parent match falls through to the existing dynamic-property check, matching property_exists(). Fixes GH-22441 Closes GH-22451 --- NEWS | 2 ++ ext/reflection/php_reflection.c | 31 +++++++++++----------- ext/reflection/tests/gh22441.phpt | 44 +++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 ext/reflection/tests/gh22441.phpt diff --git a/NEWS b/NEWS index 39f2359152f0..0f39334377e0 100644 --- a/NEWS +++ b/NEWS @@ -18,6 +18,8 @@ PHP NEWS - Reflection: . Fixed bug GH-22324 (Ignore leading namespace separator in ReflectionParameter::__construct()). (jorgsowa) + . Fixed bug GH-22441 (ReflectionClass::hasProperty() and getProperty() ignore + dynamic properties shadowing a private parent property). (iliaal) - SPL: . Fix class_parents for classes with leading slash in non-autoload mode. diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index eba5600c16a2..e747a642778c 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -4699,19 +4699,17 @@ ZEND_METHOD(ReflectionClass, hasProperty) } GET_REFLECTION_OBJECT_PTR(ce); - if ((property_info = zend_hash_find_ptr(&ce->properties_info, name)) != NULL) { - if ((property_info->flags & ZEND_ACC_PRIVATE) && property_info->ce != ce) { - RETURN_FALSE; - } + if ((property_info = zend_hash_find_ptr(&ce->properties_info, name)) != NULL + && (!(property_info->flags & ZEND_ACC_PRIVATE) + || property_info->ce == ce)) { RETURN_TRUE; - } else { - if (Z_TYPE(intern->obj) != IS_UNDEF) { - if (Z_OBJ_HANDLER(intern->obj, has_property)(Z_OBJ(intern->obj), name, 2, NULL)) { - RETURN_TRUE; - } + } + if (Z_TYPE(intern->obj) != IS_UNDEF) { + if (Z_OBJ_HANDLER(intern->obj, has_property)(Z_OBJ(intern->obj), name, 2, NULL)) { + RETURN_TRUE; } - RETURN_FALSE; } + RETURN_FALSE; } /* }}} */ @@ -4730,12 +4728,13 @@ ZEND_METHOD(ReflectionClass, getProperty) } GET_REFLECTION_OBJECT_PTR(ce); - if ((property_info = zend_hash_find_ptr(&ce->properties_info, name)) != NULL) { - if (!(property_info->flags & ZEND_ACC_PRIVATE) || property_info->ce == ce) { - reflection_property_factory(ce, name, property_info, return_value); - return; - } - } else if (Z_TYPE(intern->obj) != IS_UNDEF) { + if ((property_info = zend_hash_find_ptr(&ce->properties_info, name)) != NULL + && (!(property_info->flags & ZEND_ACC_PRIVATE) + || property_info->ce == ce)) { + reflection_property_factory(ce, name, property_info, return_value); + return; + } + if (Z_TYPE(intern->obj) != IS_UNDEF) { /* Check for dynamic properties */ if (zend_hash_exists(Z_OBJ_HT(intern->obj)->get_properties(Z_OBJ(intern->obj)), name)) { reflection_property_factory(ce, name, NULL, return_value); diff --git a/ext/reflection/tests/gh22441.phpt b/ext/reflection/tests/gh22441.phpt new file mode 100644 index 000000000000..9ca3cb0eee74 --- /dev/null +++ b/ext/reflection/tests/gh22441.phpt @@ -0,0 +1,44 @@ +--TEST-- +GH-22441 (ReflectionClass::hasProperty()/getProperty() ignore dynamic properties shadowing a private parent property) +--FILE-- +shadow = true; +$o->noShadow = true; + +$r = new ReflectionObject($o); + +echo "hasProperty:\n"; +echo "shadow (dynamic over private parent): "; var_dump($r->hasProperty('shadow')); +echo "noShadow (plain dynamic): "; var_dump($r->hasProperty('noShadow')); +echo "onlyBase (private parent, no dynamic): "; var_dump($r->hasProperty('onlyBase')); + +echo "\ngetProperty:\n"; +foreach (['shadow', 'noShadow', 'onlyBase'] as $name) { + try { + $p = $r->getProperty($name); + printf("%s: %s::\$%s\n", $name, $p->getDeclaringClass()->getName(), $p->getName()); + } catch (ReflectionException $e) { + printf("%s: %s\n", $name, $e->getMessage()); + } +} +?> +--EXPECT-- +hasProperty: +shadow (dynamic over private parent): bool(true) +noShadow (plain dynamic): bool(true) +onlyBase (private parent, no dynamic): bool(false) + +getProperty: +shadow: Child::$shadow +noShadow: Child::$noShadow +onlyBase: Property Child::$onlyBase does not exist