diff --git a/.github/workflows/main-php-matrix-windows.yml b/.github/workflows/main-php-matrix-windows.yml index 60ee632e..a079baae 100644 --- a/.github/workflows/main-php-matrix-windows.yml +++ b/.github/workflows/main-php-matrix-windows.yml @@ -21,7 +21,7 @@ on: default: 'windows-2019' env: - PHP_SDK_BINARY_TOOLS_VER: 2.2.0 + PHP_SDK_BINARY_TOOLS_VER: 2.3.0 PTHREAD_W32_VER: 3.0.0 jobs: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d1b3b57d..202d35e8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,6 +15,7 @@ jobs: - 8.1.30 - 8.2.25 - 8.3.13 + - 8.4.0RC4 uses: ./.github/workflows/main-php-matrix.yml with: @@ -29,13 +30,20 @@ jobs: include: - php: 8.1.30 vs-crt: vs16 + runs-on: windows-2019 - php: 8.2.25 vs-crt: vs16 + runs-on: windows-2019 - php: 8.3.13 vs-crt: vs16 + runs-on: windows-2019 + - php: 8.4.0RC4 + vs-crt: vs17 + runs-on: windows-2022 #duct tape until we can get vs16 on 2022 image uses: ./.github/workflows/main-php-matrix-windows.yml with: php: ${{ matrix.php }} vs-crt: ${{ matrix.vs-crt }} + runs-on: ${{ matrix.runs-on }} secrets: inherit diff --git a/src/copy.c b/src/copy.c index b8b4243d..080888c5 100644 --- a/src/copy.c +++ b/src/copy.c @@ -351,7 +351,13 @@ static zval* pmmpthread_copy_literals(const pmmpthread_ident_t* owner, zval *old memcpy(memory, old, sizeof(zval) * last); while (literal < end) { - if (pmmpthread_copy_zval(owner, literal, old_literal) == FAILURE) { + if (Z_TYPE_P(old_literal) == IS_UNDEF) { + /* + * Literals may have unused holes in 8.4 due to compiler optimizations + * See https://github.com/php/php-src/commit/1e7aac315ef1 (zend_compile_rope_finalize) + */ + ZVAL_UNDEF(literal); + } else if (pmmpthread_copy_zval(owner, literal, old_literal) == FAILURE) { zend_error_at_noreturn( E_CORE_ERROR, filename, @@ -437,6 +443,9 @@ static zend_op* pmmpthread_copy_opcodes(zend_op_array *op_array, zval *literals, case ZEND_JMP_NULL: #if PHP_VERSION_ID >= 80300 case ZEND_BIND_INIT_STATIC_OR_JMP: +#endif +#if PHP_VERSION_ID >= 80400 + case ZEND_JMP_FRAMELESS: #endif opline->op2.jmp_addr = ©[opline->op2.jmp_addr - op_array->opcodes]; break; diff --git a/src/handlers.c b/src/handlers.c index dbea1f6a..2d06bc47 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -48,7 +48,11 @@ HashTable* pmmpthread_read_debug(PMMPTHREAD_READ_DEBUG_PASSTHRU_D) { HashTable* pmmpthread_read_properties(PMMPTHREAD_READ_PROPERTIES_PASSTHRU_D) { pmmpthread_zend_object_t* threaded = PMMPTHREAD_FETCH_FROM(object); +#if PHP_VERSION_ID >= 80400 + zend_std_get_properties_ex(&threaded->std); +#else rebuild_object_properties(&threaded->std); +#endif pmmpthread_store_tohash( &threaded->std, threaded->std.properties); @@ -89,7 +93,7 @@ zval* pmmpthread_read_property(PMMPTHREAD_READ_PROPERTY_PASSTHRU_D) { zend_property_info* info = zend_get_property_info(object->ce, member, 0); if (info == ZEND_WRONG_PROPERTY_INFO) { rv = &EG(uninitialized_zval); - } else if (info == NULL || (info->flags & ZEND_ACC_STATIC) != 0) { //dynamic property + } else if (info == NULL || !PMMPTHREAD_OBJECT_PROPERTY(info)) { //dynamic property if (pmmpthread_store_read(object, &zmember, type, rv) == FAILURE) { if (type != BP_VAR_IS) { zend_error(E_WARNING, "Undefined property: %s::$%s", ZSTR_VAL(object->ce->name), ZSTR_VAL(member)); @@ -157,7 +161,7 @@ zval* pmmpthread_write_property(PMMPTHREAD_WRITE_PROPERTY_PASSTHRU_D) { bool ok = true; zend_property_info* info = zend_get_property_info(object->ce, member, 0); if (info != ZEND_WRONG_PROPERTY_INFO) { - if (info != NULL && (info->flags & ZEND_ACC_STATIC) == 0) { + if (info != NULL && PMMPTHREAD_OBJECT_PROPERTY(info)) { ZVAL_STR(&zmember, info->name); //use mangled name to avoid private member shadowing issues zend_execute_data* execute_data = EG(current_execute_data); @@ -228,7 +232,7 @@ int pmmpthread_has_property(PMMPTHREAD_HAS_PROPERTY_PASSTHRU_D) { } else { zend_property_info* info = zend_get_property_info(object->ce, member, 1); if (info != ZEND_WRONG_PROPERTY_INFO) { - if (info != NULL && (info->flags & ZEND_ACC_STATIC) == 0) { + if (info != NULL && PMMPTHREAD_OBJECT_PROPERTY(info)) { ZVAL_STR(&zmember, info->name); //defined property, use mangled name } isset = pmmpthread_store_isset(object, &zmember, has_set_exists); @@ -268,7 +272,7 @@ void pmmpthread_unset_property(PMMPTHREAD_UNSET_PROPERTY_PASSTHRU_D) { } else { zend_property_info* info = zend_get_property_info(object->ce, member, 0); if (info != ZEND_WRONG_PROPERTY_INFO) { - if (info != NULL && (info->flags & ZEND_ACC_STATIC) == 0) { + if (info != NULL && PMMPTHREAD_OBJECT_PROPERTY(info)) { ZVAL_STR(&zmember, info->name); //defined property, use mangled name } pmmpthread_store_delete(object, &zmember); diff --git a/src/object.c b/src/object.c index c1288586..0b9dac98 100644 --- a/src/object.c +++ b/src/object.c @@ -248,7 +248,7 @@ static inline void pmmpthread_base_write_property_defaults(pmmpthread_zend_objec zval* value; int result; - if (info->flags & ZEND_ACC_STATIC) { + if (!PMMPTHREAD_OBJECT_PROPERTY(info)) { continue; } diff --git a/src/pmmpthread.h b/src/pmmpthread.h index 33c9f1ed..840238f9 100644 --- a/src/pmmpthread.h +++ b/src/pmmpthread.h @@ -163,6 +163,12 @@ typedef struct _pmmpthread_call_t { #define PMMPTHREAD_CALL_EMPTY {empty_fcall_info, empty_fcall_info_cache} +#if PHP_VERSION_ID >= 80400 +#define PMMPTHREAD_OBJECT_PROPERTY(prop_info) ((prop_info->flags & (ZEND_ACC_STATIC | ZEND_ACC_VIRTUAL)) == 0) +#else +#define PMMPTHREAD_OBJECT_PROPERTY(prop_info) ((prop_info->flags & ZEND_ACC_STATIC) == 0) +#endif + /* this is a copy of the same struct in zend_closures.c, which unfortunately isn't exported */ typedef struct _zend_closure { zend_object std; diff --git a/src/prepare.c b/src/prepare.c index 10066104..96b5200b 100644 --- a/src/prepare.c +++ b/src/prepare.c @@ -182,43 +182,88 @@ static void prepare_class_function_table(const pmmpthread_ident_t* source, zend_ } ZEND_HASH_FOREACH_END(); } /* }}} */ -/* {{{ */ -static void prepare_class_property_table(const pmmpthread_ident_t* source, zend_class_entry *candidate, zend_class_entry *prepared) { +static zend_property_info* copy_property_info( + const pmmpthread_ident_t* source, + const zend_class_entry *candidate, + zend_class_entry *prepared, + const zend_property_info* info +) { + zend_property_info* dup = zend_hash_index_find_ptr(&PMMPTHREAD_ZG(resolve), (zend_ulong)info); + if (dup) { + return dup; + } - zend_property_info *info; - zend_string *name; - ZEND_HASH_FOREACH_STR_KEY_PTR(&candidate->properties_info, name, info) { - zend_property_info *dup; + if (info->ce->type == ZEND_INTERNAL_CLASS) { + dup = pemalloc(sizeof(zend_property_info), 1); + } + else { + dup = zend_arena_alloc(&CG(arena), sizeof(zend_property_info)); + } + memcpy(dup, info, sizeof(zend_property_info)); - if (info->ce->type == ZEND_INTERNAL_CLASS) { - dup = pemalloc(sizeof(zend_property_info), 1); - } else { - dup = zend_arena_alloc(&CG(arena), sizeof(zend_property_info)); - } - memcpy(dup, info, sizeof(zend_property_info)); + zend_hash_index_update_ptr(&PMMPTHREAD_ZG(resolve), (zend_ulong)info, dup); - dup->name = pmmpthread_copy_string(info->name); - if (info->doc_comment) { - if (PMMPTHREAD_ZG(options) & PMMPTHREAD_INHERIT_COMMENTS) { - dup->doc_comment = pmmpthread_copy_string(info->doc_comment); - } else dup->doc_comment = NULL; + dup->name = pmmpthread_copy_string(info->name); + if (info->doc_comment) { + if (PMMPTHREAD_ZG(options) & PMMPTHREAD_INHERIT_COMMENTS) { + dup->doc_comment = pmmpthread_copy_string(info->doc_comment); } + else dup->doc_comment = NULL; + } - if (info->ce) { - if (info->ce == candidate) { - dup->ce = prepared; - } else dup->ce = pmmpthread_prepared_entry(source, info->ce); + if (info->ce) { + if (info->ce == candidate) { + dup->ce = prepared; } + else dup->ce = pmmpthread_prepared_entry(source, info->ce); + } - pmmpthread_copy_zend_type(&info->type, &dup->type); + pmmpthread_copy_zend_type(&info->type, &dup->type); - if (info->attributes) { - dup->attributes = pmmpthread_copy_attributes(source, info->attributes, info->ce->type == ZEND_INTERNAL_CLASS ? NULL : info->ce->info.user.filename); + if (info->attributes) { + dup->attributes = pmmpthread_copy_attributes(source, info->attributes, info->ce->type == ZEND_INTERNAL_CLASS ? NULL : info->ce->info.user.filename); + } + +#if PHP_VERSION_ID >= 80400 + if (info->prototype) { + dup->prototype = copy_property_info(source, candidate, prepared, info->prototype); + } else dup->prototype = NULL; + + if (info->hooks) { + dup->hooks = zend_arena_alloc(&CG(arena), ZEND_PROPERTY_HOOK_STRUCT_SIZE); + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (info->hooks[i]) { + const zend_function* original_hook = info->hooks[i]; + zend_function* copy_hook = pmmpthread_copy_function(source, original_hook); + + if (original_hook->type == ZEND_USER_FUNCTION) { + ZEND_ASSERT(original_hook->op_array.prop_info); + copy_hook->op_array.prop_info = copy_property_info(source, candidate, prepared, info->hooks[i]->op_array.prop_info); + } + + dup->hooks[i] = copy_hook; + } else dup->hooks[i] = NULL; } + } else dup->hooks = NULL; +#endif - if (!zend_hash_str_add_ptr(&prepared->properties_info, name->val, name->len, dup)) { - if (dup->doc_comment) - zend_string_release(dup->doc_comment); + return dup; +} +/* {{{ */ +static void prepare_class_property_table(const pmmpthread_ident_t* source, zend_class_entry *candidate, zend_class_entry *prepared) { + + zend_property_info *info; + zend_string *name; + ZEND_HASH_FOREACH_STR_KEY_PTR(&candidate->properties_info, name, info) { + zend_property_info* dup = zend_hash_find_ptr(&prepared->properties_info, name); + //TODO: if this is non-null it may need updating (if we copied it previously for an unlinked class) + //for now this just ensures that we don't have UAFs with reused property infos + //hopefully this doesn't shit a brick??? + if (dup == NULL) { + dup = copy_property_info(source, candidate, prepared, info); + if (!zend_hash_str_add_ptr(&prepared->properties_info, name->val, name->len, dup)) { + ZEND_ASSERT(0); + } } } ZEND_HASH_FOREACH_END(); @@ -257,7 +302,7 @@ static void prepare_class_property_table(const pmmpthread_ident_t* source, zend_ } ZEND_HASH_FOREACH_PTR(&prepared->properties_info, info) { - if (info->ce == prepared && (info->flags & ZEND_ACC_STATIC) == 0) { + if (info->ce == prepared && PMMPTHREAD_OBJECT_PROPERTY(info)) { prepared->properties_info_table[OBJ_PROP_TO_NUM(info->offset)] = info; } } ZEND_HASH_FOREACH_END(); @@ -479,11 +524,18 @@ static zend_class_entry* pmmpthread_copy_entry(const pmmpthread_ident_t* source, prepared->refcount = 1; memcpy(&prepared->info.user, &candidate->info.user, sizeof(candidate->info.user)); - if ((PMMPTHREAD_ZG(options) & PMMPTHREAD_INHERIT_COMMENTS) && - (candidate->info.user.doc_comment)) { - prepared->info.user.doc_comment = pmmpthread_copy_string(candidate->info.user.doc_comment); - } else prepared->info.user.doc_comment = NULL; +#if PHP_VERSION_ID >= 80400 + (candidate->doc_comment)) { + prepared->doc_comment = pmmpthread_copy_string(candidate->doc_comment); + } else prepared->doc_comment = NULL; + prepared->num_hooked_props = candidate->num_hooked_props; + prepared->num_hooked_prop_variance_checks = candidate->num_hooked_prop_variance_checks; +#else + (candidate->info.user.doc_comment)) { + prepared->info.user.doc_comment = pmmpthread_copy_string(candidate->info.user.doc_comment); + } else prepared->info.user.doc_comment = NULL; +#endif if (candidate->attributes) { prepared->attributes = pmmpthread_copy_attributes(source, candidate->attributes, prepared->info.user.filename); diff --git a/src/store.c b/src/store.c index 20e15495..002501c2 100644 --- a/src/store.c +++ b/src/store.c @@ -58,7 +58,11 @@ void pmmpthread_store_destroy(pmmpthread_store_t* store) { /* {{{ Prepares local property table to cache items. We may use integer keys, so the ht must be explicitly initialized to avoid zend allocating it as packed, which will cause assert failures. */ static void pmmpthread_store_init_local_properties(zend_object* object) { +#if PHP_VERSION_ID >= 80400 + zend_std_get_properties_ex(object); +#else rebuild_object_properties(object); +#endif if (HT_FLAGS(object->properties) & HASH_FLAG_UNINITIALIZED) { zend_hash_real_init_mixed(object->properties); } @@ -143,7 +147,7 @@ static inline zend_bool pmmpthread_store_retain_in_local_cache(zval* val) { } static inline zend_bool pmmpthread_store_valid_local_cache_item(zval* val) { - //rebuild_object_properties() may add IS_INDIRECT zvals to point to the linear property table + //zend_std_get_properties_ex() may add IS_INDIRECT zvals to point to the linear property table //we don't want that, because they aren't used by pmmpthread and are always uninitialized return Z_TYPE_P(val) != IS_INDIRECT; } @@ -831,7 +835,7 @@ void pmmpthread_store_tohash(zend_object *object, HashTable *hash) { for (int i = 0; i < object->ce->default_properties_count; i++) { zend_property_info* info = object->ce->properties_info_table[i]; - if (info == NULL || (info->flags & ZEND_ACC_STATIC) != 0) { + if (info == NULL || !PMMPTHREAD_OBJECT_PROPERTY(info)) { continue; } diff --git a/tests/closure-ts-this.phpt b/tests/closure-ts-this.phpt index 60d96642..324f8e2d 100644 --- a/tests/closure-ts-this.phpt +++ b/tests/closure-ts-this.phpt @@ -29,8 +29,8 @@ $thread->start(\pmmp\thread\Thread::INHERIT_ALL); $thread->join(); ?> ---EXPECT-- -object(Closure)#4 (1) { +--EXPECTF-- +object(Closure)#4 (%d) {%A ["this"]=> object(A)#3 (0) { } diff --git a/tests/closure-with-use-wrapped.phpt b/tests/closure-with-use-wrapped.phpt index aabb2680..ee68c9c5 100644 --- a/tests/closure-with-use-wrapped.phpt +++ b/tests/closure-with-use-wrapped.phpt @@ -40,10 +40,10 @@ $closureWithUse = static function () use ($test): void{ }; wrap($closureWithUse, $worker); ?> ---EXPECT-- -object(Closure)#4 (0) { +--EXPECTF-- +object(Closure)#4 (%d) {%A } -object(Closure)#7 (1) { +object(Closure)#7 (%d) {%A ["static"]=> array(1) { ["test"]=> diff --git a/tests/lexical-vars.phpt b/tests/lexical-vars.phpt index 694ee04b..d9543e2d 100644 --- a/tests/lexical-vars.phpt +++ b/tests/lexical-vars.phpt @@ -51,7 +51,7 @@ string(5) "thing" NULL object(pmmp\thread\ThreadSafeArray)#3 (0) { } -object(Closure)#4 (0) { +object(Closure)#4 (%d) {%A } array(5) { [0]=> diff --git a/tests/new-in-attributes.phpt b/tests/new-in-attributes.phpt index 81bd9452..5ee3737b 100644 --- a/tests/new-in-attributes.phpt +++ b/tests/new-in-attributes.phpt @@ -28,20 +28,20 @@ $w->start(\pmmp\thread\Thread::INHERIT_ALL) && $w->join(); test(); ?> ---EXPECT-- +--EXPECTF-- array(1) { [0]=> - object(Attr)#4 (1) { + object(Attr)#%d (1) { ["object"]=> - object(stdClass)#6 (0) { + object(stdClass)#%d (0) { } } } array(1) { [0]=> - object(Attr)#4 (1) { + object(Attr)#%d (1) { ["object"]=> - object(stdClass)#6 (0) { + object(stdClass)#%d (0) { } } } diff --git a/tests/property-hooks.phpt b/tests/property-hooks.phpt new file mode 100644 index 00000000..3d5fe2c1 --- /dev/null +++ b/tests/property-hooks.phpt @@ -0,0 +1,106 @@ +--TEST-- +Test that PHP 8.4 property read/write hooks work as expected on copied classes +--SKIPIF-- + +--FILE-- + $this->virtualBackingAsymmetric + 1; + //can't be set because it's not backed + } + + public int $virtualOnlySet { + //do NOT use an arrow function for this - it will make the property non-virtual + //thanks for the new footgun PHP!!! + set { $this->virtualBackingAsymmetric = $value - 1; } + } + + private int $virtualBacking = 0; + + public int $virtualReadWrite { + get => $this->virtualBacking + 1; + set => $this->virtualBacking = $value - 1; + } + + public int $backedOnlyGet { + get => $this->backedOnlyGet + 1; + //set will use default property write behaviour + } + + public int $backedOnlySet { + set => $value + 1; + } + + public int $backedGetSet { + get => $this->backedGetSet + 1; + set => $value - 1; + } +} + +function test(PropertyHooks $object) : void{ + var_dump($object); + + try{ + $object->virtualOnlyGet = 1; //error + echo "Something is wrong, this is not supposed to be writable\n"; + }catch(\Error $e){ + echo $e->getMessage() . "\n"; + } + + //TODO: test more stuff +} + +echo "--- main thread test ---\n"; +test(new PropertyHooks()); +echo "--- main thread done ---\n"; + +$t = new class extends \pmmp\thread\Thread{ + public function run() : void{ + echo "--- child thread test ---\n"; + test(new PropertyHooks()); + echo "--- child thread done ---\n"; + } +}; +$t->start(\pmmp\thread\Thread::INHERIT_ALL) && $t->join(); +echo "done\n"; +--EXPECTF-- +--- main thread test --- +object(PropertyHooks)#%d (2) { + ["virtualBackingAsymmetric":"PropertyHooks":private]=> + int(0) + ["virtualBacking":"PropertyHooks":private]=> + int(0) + ["virtualReadWrite"]=> + uninitialized(int) + ["backedOnlyGet"]=> + uninitialized(int) + ["backedOnlySet"]=> + uninitialized(int) + ["backedGetSet"]=> + uninitialized(int) +} +Property PropertyHooks::$virtualOnlyGet is read-only +--- main thread done --- +--- child thread test --- +object(PropertyHooks)#%d (2) { + ["virtualBackingAsymmetric":"PropertyHooks":private]=> + int(0) + ["virtualBacking":"PropertyHooks":private]=> + int(0) + ["virtualReadWrite"]=> + uninitialized(int) + ["backedOnlyGet"]=> + uninitialized(int) + ["backedOnlySet"]=> + uninitialized(int) + ["backedGetSet"]=> + uninitialized(int) +} +Property PropertyHooks::$virtualOnlyGet is read-only +--- child thread done --- +done diff --git a/tests/readonly-properties.phpt b/tests/readonly-properties.phpt index 0d5543ae..7e8f26fd 100644 --- a/tests/readonly-properties.phpt +++ b/tests/readonly-properties.phpt @@ -41,10 +41,10 @@ $t->start(\pmmp\thread\Thread::INHERIT_ALL) && $t->join(); test(); ?> ---EXPECT-- +--EXPECTF-- int(1) string(40) "Cannot modify readonly property Test::$a" -string(62) "Cannot initialize readonly property Test::$b from global scope" +string(%d) "Cannot %s readonly property Test::$b from global scope" int(1) string(40) "Cannot modify readonly property Test::$a" -string(62) "Cannot initialize readonly property Test::$b from global scope" +string(%d) "Cannot %s readonly property Test::$b from global scope" diff --git a/tests/var-dump-consistency.phpt b/tests/var-dump-consistency.phpt index 6ba6027f..2c85b6ad 100644 --- a/tests/var-dump-consistency.phpt +++ b/tests/var-dump-consistency.phpt @@ -13,18 +13,18 @@ var_dump($t["sock"]); var_dump($t); ?> ---EXPECT-- +--EXPECTF-- object(stdClass)#4 (0) { } object(pmmp\thread\ThreadSafeArray)#2 (1) { ["sock"]=> - object(Closure)#3 (0) { + object(Closure)#3 (%d) {%A } } -object(Closure)#3 (0) { +object(Closure)#3 (%d) {%A } object(pmmp\thread\ThreadSafeArray)#2 (1) { ["sock"]=> - object(Closure)#3 (0) { + object(Closure)#3 (%d) {%A } }