diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 2f682bc..2e68a4c 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -68,67 +68,6 @@ parameters: - identifier: offsetAccess.nonOffsetAccessible path: src/Type/StructArray.php - # PayloadRelocator is the file-cache pointer-surgery machinery: it walks - # engine structs field by field through FFI\CData, so a chained access - # (`$zval->u1->v->type`) resolves to `mixed` after the first hop and the - # arithmetic/casts on those reads cannot be statically typed. The FFI - # blast radius is confined to this one audited file (AGENTS.md); every - # path is covered by the byte-identity and execute-from-cache tests. - - - identifier: property.nonObject - path: src/OpCache/PayloadRelocator.php - - - identifier: binaryOp.invalid - path: src/OpCache/PayloadRelocator.php - - - identifier: cast.int - path: src/OpCache/PayloadRelocator.php - - - identifier: argument.type - path: src/OpCache/PayloadRelocator.php - # ScriptSerializer is the second audited pointer-surgery file (the - # persist-from-graph writer, issue #117): the same CData field walking - # as PayloadRelocator, covered by the rebuild/graft execute-from-cache - # tests and the relocator round-trip identity checks. - - - identifier: property.nonObject - path: src/OpCache/ScriptSerializer.php - - - identifier: binaryOp.invalid - path: src/OpCache/ScriptSerializer.php - - - identifier: cast.int - path: src/OpCache/ScriptSerializer.php - - - identifier: argument.type - path: src/OpCache/ScriptSerializer.php - # ReflectionOpcacheFile is the CData facade over the relocated script: - # it reads embedded engine structs (script.filename, function/class - # tables) that resolve to `mixed` after the first CData hop; since - # issue #117 it also carries the graft plumbing (image hashtable - # regrowth), which does the same CData arithmetic. - - - identifier: property.nonObject - path: src/OpCache/ReflectionOpcacheFile.php - - - identifier: argument.type - path: src/OpCache/ReflectionOpcacheFile.php - - - identifier: binaryOp.invalid - path: src/OpCache/ReflectionOpcacheFile.php - - - identifier: cast.int - path: src/OpCache/ReflectionOpcacheFile.php - - - identifier: assignOp.invalid - path: src/OpCache/ReflectionOpcacheFile.php - # BoundsValidationTest crafts hostile payloads by poking engine-struct - # pointer fields through FFI\CData (the address of a filename/HashTable - # slot to overwrite), the same pointer surgery PayloadRelocator does - - # FFI::addr() on a CData field read resolves to mixed here. - - - identifier: argument.type - path: tests/OpCache/BoundsValidationTest.php # The dimension tests exist to prove that a plain `count($object)` reaches the engine's # count_elements handler on a class that never declared the count itself. Rewriting them # as assertCount() would measure PHPUnit's Count constraint instead of the language diff --git a/src/OpCache/PayloadRelocator.php b/src/OpCache/PayloadRelocator.php index 74e2d12..2ba5197 100644 --- a/src/OpCache/PayloadRelocator.php +++ b/src/OpCache/PayloadRelocator.php @@ -17,15 +17,28 @@ use FFI\CData; use ZEngine\Core; use ZEngine\Generated\Bucket; +use ZEngine\Generated\HashTable as HashTableStruct; use ZEngine\Generated\zend_arg_info; use ZEngine\Generated\zend_ast; use ZEngine\Generated\zend_ast_list; use ZEngine\Generated\zend_ast_ref; +use ZEngine\Generated\zend_ast_zval; +use ZEngine\Generated\zend_attribute; use ZEngine\Generated\zend_attribute_arg; +use ZEngine\Generated\zend_class_arrayaccess_funcs; +use ZEngine\Generated\zend_class_constant; +use ZEngine\Generated\zend_class_entry; +use ZEngine\Generated\zend_class_iterator_funcs; use ZEngine\Generated\zend_class_name; use ZEngine\Generated\zend_early_binding; +use ZEngine\Generated\zend_error_info; +use ZEngine\Generated\zend_function; +use ZEngine\Generated\zend_op_array; use ZEngine\Generated\zend_persistent_script; +use ZEngine\Generated\zend_property_info; use ZEngine\Generated\zend_string; +use ZEngine\Generated\zend_trait_alias; +use ZEngine\Generated\zend_trait_precedence; use ZEngine\Generated\zend_type; use ZEngine\Generated\zend_type_list; use ZEngine\Generated\zval; @@ -140,7 +153,7 @@ public function __construct(private readonly object $buffer, private readonly Ca * Rewrites the buffer in place, converting every stored offset to a real * address, and returns a typed pointer to the embedded zend_persistent_script. * - * @return \FFI\CData + * @return zend_persistent_script */ public function relocate(): object { @@ -152,7 +165,7 @@ public function relocate(): object Core::sizeOfType(zend_persistent_script::class), 'zend_persistent_script at scriptOffset', ); - $script = Core::pointerAtAddress('zend_persistent_script *', $this->base + $this->metaInfo->scriptOffset()); + $script = Core::pointerAtAddress(zend_persistent_script::class, $this->base + $this->metaInfo->scriptOffset()); $this->unStr($script->script, 'filename'); $this->unserializeHash($script->script->class_table, $this->unserializeClass(...)); @@ -189,7 +202,7 @@ private function serialize(): string $this->strSection = ''; $this->internedXlat = []; $this->sharedOpcodes = []; - $script = Core::pointerAtAddress('zend_persistent_script *', $this->base + $this->metaInfo->scriptOffset()); + $script = Core::pointerAtAddress(zend_persistent_script::class, $this->base + $this->metaInfo->scriptOffset()); $this->serStr($script->script, 'filename'); $this->serializeHash($script->script->class_table, $this->serializeClass(...)); @@ -198,7 +211,8 @@ private function serialize(): string $this->serializeWarnings($script); $this->serializeEarlyBindings($script); - $memRegion = FFI::string($this->buffer, $this->size); + // max(...,0) only states the non-negative mem-region size to the analyser + $memRegion = FFI::string($this->buffer, max($this->size, 0)); // Re-pin the tagged-offset bound to the section just emitted, so the // relocate() in derelocate() validates against it (issue #123) $this->strSize = strlen($this->strSection); @@ -208,12 +222,25 @@ private function serialize(): string // --- pointer/offset primitives (SERIALIZE_PTR / UNSERIALIZE_PTR) -------- + /** + * Reads a uintptr_t pointer slot as a PHP int - the raw-pointer read + * primitive. The dereferenced CData element is always an integer at runtime; + * the guard states that to the analyser without widening any real value. + * + * @param \FFI\CData $slot a uintptr_t* view over the slot to read + */ + private function readSlot(object $slot): int + { + $value = $slot[0]; + \assert(\is_int($value)); + + return $value; + } + /** * Reads a pointer field's stored value through a raw integer view of its * storage, so it works for every pointee type including void* (which * Core::addressOf cannot cast). 0 when the C NULL surfaces as PHP null. - * - * @param \FFI\CData $owner */ private function ptrValue(object $owner, string $field): int { @@ -221,15 +248,16 @@ private function ptrValue(object $owner, string $field): int return 0; } - return (int) Core::cast('uintptr_t *', FFI::addr($owner->$field))[0]; + // A dynamically-named pointer field cannot be statically resolved, so + // FFI::addr() on the mixed field read is the one irreducible CData hop. + // @phpstan-ignore argument.type (FFI::addr of a dynamic FFI\CData pointer field) + return $this->readSlot(Core::cast('uintptr_t *', FFI::addr($owner->$field))); } - /** - * @param \FFI\CData $owner - */ private function writePtrField(object $owner, string $field, int $address): void { - // Only ever called for a currently non-null field, so FFI::addr is safe + // Only ever called for a currently non-null field, so FFI::addr is safe. + // @phpstan-ignore argument.type (FFI::addr of a dynamic FFI\CData pointer field) $slot = Core::cast('uintptr_t *', FFI::addr($owner->$field)); $slot[0] = $address; } @@ -319,9 +347,6 @@ private function requireCount(int $count, string $what): int } /** UNSERIALIZE_PTR on a struct field, returning the resolved address (0 if null) */ - /** - * @param \FFI\CData $owner - */ private function unPtr(object $owner, string $field): int { $stored = $this->ptrValue($owner, $field); @@ -336,9 +361,6 @@ private function unPtr(object $owner, string $field): int } /** SERIALIZE_PTR on a struct field, returning the pre-serialization address (0 if null) */ - /** - * @param \FFI\CData $owner - */ private function serPtr(object $owner, string $field): int { $address = $this->ptrValue($owner, $field); @@ -354,7 +376,7 @@ private function serPtr(object $owner, string $field): int private function unPtrAt(int $slotAddress): int { $slot = Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $slotAddress)); - $stored = (int) $slot[0]; + $stored = $this->readSlot($slot); if ($stored === 0) { return 0; } @@ -368,7 +390,7 @@ private function unPtrAt(int $slotAddress): int private function serPtrAt(int $slotAddress): int { $slot = Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $slotAddress)); - $address = (int) $slot[0]; + $address = $this->readSlot($slot); if ($address === 0) { return 0; } @@ -378,9 +400,6 @@ private function serPtrAt(int $slotAddress): int } // --- interned-string primitives (UNSERIALIZE_STR / SERIALIZE_STR) ------ - /** - * @param \FFI\CData $owner - */ private function unStr(object $owner, string $field): void { @@ -398,9 +417,6 @@ private function unStr(object $owner, string $field): void // GC flag normalization is deliberately skipped (see class docblock) $this->writePtrField($owner, $field, $address); } - /** - * @param \FFI\CData $owner - */ private function serStr(object $owner, string $field): void { @@ -421,7 +437,7 @@ private function serStr(object $owner, string $field): void private function unStrAt(int $slotAddress): void { $slot = Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $slotAddress)); - $stored = (int) $slot[0]; + $stored = $this->readSlot($slot); if ($stored === 0) { return; } @@ -437,7 +453,7 @@ private function unStrAt(int $slotAddress): void private function serStrAt(int $slotAddress): void { $slot = Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $slotAddress)); - $address = (int) $slot[0]; + $address = $this->readSlot($slot); if ($address === 0) { return; } @@ -457,24 +473,23 @@ private function emitInterned(int $address): int if (isset($this->internedXlat[$address])) { return $this->internedXlat[$address]; } - $stringPointer = Core::pointerAtAddress('zend_string *', $address); + $stringPointer = Core::pointerAtAddress(zend_string::class, $address); $length = $stringPointer->len; $structSize = Core::getAlignedSize($this->zendStringHeaderSize + $length + 1); $tagged = strlen($this->strSection) | 1; $this->internedXlat[$address] = $tagged; - $this->strSection .= FFI::string(Core::cast('char *', $stringPointer), $structSize); + // max(...,0) only states the non-negative aligned size to the analyser + $this->strSection .= FFI::string(Core::cast('char *', $stringPointer), max($structSize, 0)); return $tagged; } // --- hashes (zend_file_cache_(un)serialize_hash) ----------------------- - /** - * @param \FFI\CData $ht - */ private function unserializeHash(object $ht, callable $each): void { + /** @var HashTableStruct $ht Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if (($ht->u->flags & self::HASH_FLAG_UNINITIALIZED) !== 0) { return; } @@ -487,7 +502,7 @@ private function unserializeHash(object $ht, callable $each): void $zvalSize = Core::sizeOfType(zval::class); $this->requireSpan($dataAddress, $used * $zvalSize, 'packed hashtable data'); for ($i = 0; $i < $used; $i++) { - $zval = Core::pointerAtAddress('zval *', $dataAddress + $i * $zvalSize); + $zval = Core::pointerAtAddress(zval::class, $dataAddress + $i * $zvalSize); if ($zval->u1->v->type !== 0) { $each($zval); } @@ -498,19 +513,17 @@ private function unserializeHash(object $ht, callable $each): void $bucketSize = Core::sizeOfType(Bucket::class); $this->requireSpan($dataAddress, $used * $bucketSize, 'hashtable bucket data'); for ($i = 0; $i < $used; $i++) { - $bucket = Core::pointerAtAddress('Bucket *', $dataAddress + $i * $bucketSize); + $bucket = Core::pointerAtAddress(Bucket::class, $dataAddress + $i * $bucketSize); if ($bucket->val->u1->v->type !== 0) { $this->unStr($bucket, 'key'); $each($bucket->val); } } } - /** - * @param \FFI\CData $ht - */ private function serializeHash(object $ht, callable $each): void { + /** @var HashTableStruct $ht Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if (($ht->u->flags & self::HASH_FLAG_UNINITIALIZED) !== 0) { $this->writePtrField($ht, 'arData', 0); @@ -524,7 +537,7 @@ private function serializeHash(object $ht, callable $each): void if (($ht->u->flags & self::HASH_FLAG_PACKED) !== 0) { $zvalSize = Core::sizeOfType(zval::class); for ($i = 0; $i < $used; $i++) { - $zval = Core::pointerAtAddress('zval *', $dataAddress + $i * $zvalSize); + $zval = Core::pointerAtAddress(zval::class, $dataAddress + $i * $zvalSize); if ($zval->u1->v->type !== 0) { $each($zval); } @@ -534,7 +547,7 @@ private function serializeHash(object $ht, callable $each): void } $bucketSize = Core::sizeOfType(Bucket::class); for ($i = 0; $i < $used; $i++) { - $bucket = Core::pointerAtAddress('Bucket *', $dataAddress + $i * $bucketSize); + $bucket = Core::pointerAtAddress(Bucket::class, $dataAddress + $i * $bucketSize); if ($bucket->val->u1->v->type !== 0) { $this->serStr($bucket, 'key'); $each($bucket->val); @@ -543,12 +556,10 @@ private function serializeHash(object $ht, callable $each): void } // --- zvals ------------------------------------------------------------- - /** - * @param \FFI\CData $zval - */ private function unserializeZval(object $zval): void { + /** @var zval $zval Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ switch ($zval->u1->v->type) { case self::IS_STRING: $stored = $this->ptrValue($zval->value, 'str'); @@ -560,7 +571,7 @@ private function unserializeZval(object $zval): void if (!$this->isUnserialized($this->ptrValue($zval->value, 'arr'))) { $arrAddress = $this->unPtr($zval->value, 'arr'); $this->unserializeHash( - Core::pointerAtAddress('zend_array *', $arrAddress), + Core::pointerAtAddress(HashTableStruct::class, $arrAddress), $this->unserializeZval(...), ); } @@ -576,12 +587,10 @@ private function unserializeZval(object $zval): void break; } } - /** - * @param \FFI\CData $zval - */ private function serializeZval(object $zval): void { + /** @var zval $zval Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ switch ($zval->u1->v->type) { case self::IS_STRING: if (!$this->isSerialized($this->ptrValue($zval->value, 'str'))) { @@ -592,7 +601,7 @@ private function serializeZval(object $zval): void if (!$this->isSerialized($this->ptrValue($zval->value, 'arr'))) { $arrAddress = $this->serPtr($zval->value, 'arr'); $this->serializeHash( - Core::pointerAtAddress('zend_array *', $arrAddress), + Core::pointerAtAddress(HashTableStruct::class, $arrAddress), $this->serializeZval(...), ); } @@ -616,15 +625,15 @@ private function unserializeAst(int $astAddress): void { // The node header (kind + attr) must fit before it is read $this->requireSpan($astAddress, Core::sizeOfType(zend_ast::class), 'zend_ast node'); - $ast = Core::pointerAtAddress('zend_ast *', $astAddress); + $ast = Core::pointerAtAddress(zend_ast::class, $astAddress); $kind = $ast->kind; if ($kind === self::ZEND_AST_ZVAL || $kind === self::ZEND_AST_CONSTANT) { - $this->unserializeZval(Core::pointerAtAddress('zend_ast_zval *', $astAddress)->val); + $this->unserializeZval(Core::pointerAtAddress(zend_ast_zval::class, $astAddress)->val); return; } if (($kind >> self::ZEND_AST_IS_LIST_SHIFT & 1) !== 0) { - $list = Core::pointerAtAddress('zend_ast_list *', $astAddress); + $list = Core::pointerAtAddress(zend_ast_list::class, $astAddress); $childBase = $astAddress + Core::sizeOfType(zend_ast_list::class) - PHP_INT_SIZE; $count = $this->requireCount((int) $list->children, 'ast list children'); } else { @@ -634,7 +643,7 @@ private function unserializeAst(int $astAddress): void $this->requireSpan($childBase, $count * PHP_INT_SIZE, 'ast children slots'); for ($i = 0; $i < $count; $i++) { $slot = Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $childBase + $i * PHP_INT_SIZE)); - $child = (int) $slot[0]; + $child = $this->readSlot($slot); if ($child !== 0 && !$this->isUnserialized($child)) { $this->requireOffset($child, 'ast child'); $slot[0] = $this->base + $child; @@ -645,15 +654,15 @@ private function unserializeAst(int $astAddress): void private function serializeAst(int $astAddress): void { - $ast = Core::pointerAtAddress('zend_ast *', $astAddress); + $ast = Core::pointerAtAddress(zend_ast::class, $astAddress); $kind = $ast->kind; if ($kind === self::ZEND_AST_ZVAL || $kind === self::ZEND_AST_CONSTANT) { - $this->serializeZval(Core::pointerAtAddress('zend_ast_zval *', $astAddress)->val); + $this->serializeZval(Core::pointerAtAddress(zend_ast_zval::class, $astAddress)->val); return; } if (($kind >> self::ZEND_AST_IS_LIST_SHIFT & 1) !== 0) { - $list = Core::pointerAtAddress('zend_ast_list *', $astAddress); + $list = Core::pointerAtAddress(zend_ast_list::class, $astAddress); $childBase = $astAddress + Core::sizeOfType(zend_ast_list::class) - PHP_INT_SIZE; $count = $list->children; } else { @@ -662,7 +671,7 @@ private function serializeAst(int $astAddress): void } for ($i = 0; $i < $count; $i++) { $slot = Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $childBase + $i * PHP_INT_SIZE)); - $child = (int) $slot[0]; + $child = $this->readSlot($slot); if ($child !== 0 && !$this->isSerialized($child)) { $slot[0] = $child - $this->base; $this->serializeAst($child); @@ -671,9 +680,6 @@ private function serializeAst(int $astAddress): void } // --- attributes -------------------------------------------------------- - /** - * @param \FFI\CData $owner - */ private function unserializeAttributes(object $owner, string $field): void { @@ -683,13 +689,10 @@ private function unserializeAttributes(object $owner, string $field): void } $htAddress = $this->unPtr($owner, $field); $this->unserializeHash( - Core::pointerAtAddress('HashTable *', $htAddress), + Core::pointerAtAddress(HashTableStruct::class, $htAddress), $this->unserializeAttribute(...), ); } - /** - * @param \FFI\CData $owner - */ private function serializeAttributes(object $owner, string $field): void { @@ -699,17 +702,15 @@ private function serializeAttributes(object $owner, string $field): void } $htAddress = $this->serPtr($owner, $field); $this->serializeHash( - Core::pointerAtAddress('HashTable *', $htAddress), + Core::pointerAtAddress(HashTableStruct::class, $htAddress), $this->serializeAttribute(...), ); } - /** - * @param \FFI\CData $zval - */ private function unserializeAttribute(object $zval): void { - $attr = Core::pointerAtAddress('zend_attribute *', $this->unPtr($zval->value, 'ptr')); + /** @var zval $zval Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ + $attr = Core::pointerAtAddress(zend_attribute::class, $this->unPtr($zval->value, 'ptr')); $this->unStr($attr, 'name'); $this->unStr($attr, 'lcname'); $argSize = Core::sizeOfType(zend_attribute_arg::class); @@ -717,69 +718,52 @@ private function unserializeAttribute(object $zval): void $argc = $this->requireCount((int) $attr->argc, 'attribute argc'); $this->requireSpan($argBase, $argc * $argSize, 'attribute args'); for ($i = 0; $i < $argc; $i++) { - $arg = Core::pointerAtAddress('zend_attribute_arg *', $argBase + $i * $argSize); + $arg = Core::pointerAtAddress(zend_attribute_arg::class, $argBase + $i * $argSize); $this->unStr($arg, 'name'); $this->unserializeZval($arg->value); } } - /** - * @param \FFI\CData $zval - */ private function serializeAttribute(object $zval): void { + /** @var zval $zval Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ $address = $this->serPtr($zval->value, 'ptr'); - $attr = Core::pointerAtAddress('zend_attribute *', $address); + $attr = Core::pointerAtAddress(zend_attribute::class, $address); $this->serStr($attr, 'name'); $this->serStr($attr, 'lcname'); $argSize = Core::sizeOfType(zend_attribute_arg::class); $argBase = Core::addressOf($attr->args); for ($i = 0; $i < $attr->argc; $i++) { - $arg = Core::pointerAtAddress('zend_attribute_arg *', $argBase + $i * $argSize); + $arg = Core::pointerAtAddress(zend_attribute_arg::class, $argBase + $i * $argSize); $this->serStr($arg, 'name'); $this->serializeZval($arg->value); } } // --- types (zend_type name/list) --------------------------------------- - /** - * @param \FFI\CData $owner - */ - - private function unserializeType(object $owner, string $field): void - { - $this->unserializeTypeStruct($owner->$field); - } - /** - * @param \FFI\CData $owner - */ - - private function serializeType(object $owner, string $field): void - { - $this->serializeTypeStruct($owner->$field); - } /** * One zend_type in place - the ZEND_TYPE_HAS_LIST branch of * zend_file_cache_unserialize_type relocates the zend_type_list pointer and * recurses into every entry, so DNF sub-lists like (A&B)|C unfold naturally. * - * @param \FFI\CData $type a zend_type view (embedded field or list entry) + * @param zend_type $type a zend_type view (embedded field or list entry) */ private function unserializeTypeStruct(object $type): void { + /** @var zend_type $type Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ $typeMask = $type->type_mask; if (($typeMask & self::TYPE_LIST_BIT) !== 0) { $listAddress = $this->unPtr($type, 'ptr'); $this->requireSpan($listAddress, Core::sizeOfType(zend_type_list::class), 'zend_type_list header'); - $list = Core::pointerAtAddress('zend_type_list *', $listAddress); + $list = Core::pointerAtAddress(zend_type_list::class, $listAddress); $typeSize = Core::sizeOfType(zend_type::class); // ZEND_TYPE_LIST_FOREACH: entries start at list->types (the flexible member) $entryBase = $listAddress + Core::sizeOfType(zend_type_list::class) - $typeSize; $numTypes = $this->requireCount((int) $list->num_types, 'type list num_types'); $this->requireSpan($entryBase, $numTypes * $typeSize, 'type list entries'); for ($i = 0; $i < $numTypes; $i++) { - $this->unserializeTypeStruct(Core::pointerAtAddress('zend_type *', $entryBase + $i * $typeSize)); + $this->unserializeTypeStruct(Core::pointerAtAddress(zend_type::class, $entryBase + $i * $typeSize)); } return; @@ -794,18 +778,19 @@ private function unserializeTypeStruct(object $type): void * stores the list pointer as an offset but keeps walking the entries through * the still-real address (its SERIALIZE_PTR/UNSERIALIZE_PTR pair). * - * @param \FFI\CData $type a zend_type view (embedded field or list entry) + * @param zend_type $type a zend_type view (embedded field or list entry) */ private function serializeTypeStruct(object $type): void { + /** @var zend_type $type Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ $typeMask = $type->type_mask; if (($typeMask & self::TYPE_LIST_BIT) !== 0) { $listAddress = $this->serPtr($type, 'ptr'); - $list = Core::pointerAtAddress('zend_type_list *', $listAddress); + $list = Core::pointerAtAddress(zend_type_list::class, $listAddress); $typeSize = Core::sizeOfType(zend_type::class); $entryBase = $listAddress + Core::sizeOfType(zend_type_list::class) - $typeSize; for ($i = 0; $i < $list->num_types; $i++) { - $this->serializeTypeStruct(Core::pointerAtAddress('zend_type *', $entryBase + $i * $typeSize)); + $this->serializeTypeStruct(Core::pointerAtAddress(zend_type::class, $entryBase + $i * $typeSize)); } return; @@ -816,30 +801,24 @@ private function serializeTypeStruct(object $type): void } // --- op_array (the executable body) ------------------------------------ - /** - * @param \FFI\CData $zval - */ private function unserializeFunc(object $zval): void { - $func = Core::pointerAtAddress('zend_function *', $this->unPtr($zval->value, 'func')); + /** @var zval $zval Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ + $func = Core::pointerAtAddress(zend_function::class, $this->unPtr($zval->value, 'func')); $this->unserializeOpArray($func->op_array); } - /** - * @param \FFI\CData $zval - */ private function serializeFunc(object $zval): void { - $func = Core::pointerAtAddress('zend_function *', $this->serPtr($zval->value, 'func')); + /** @var zval $zval Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ + $func = Core::pointerAtAddress(zend_function::class, $this->serPtr($zval->value, 'func')); $this->serializeOpArray($func->op_array); } - /** - * @param \FFI\CData $opArray - */ private function unserializeOpArray(object $opArray): void { + /** @var zend_op_array $opArray Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ // ZEND_MAP_PTR / run-time cache normalization is skipped (never executed here) if ($this->isUnserialized($this->ptrValue($opArray, 'opcodes'))) { return; // shared method body already relocated @@ -866,7 +845,7 @@ private function unserializeOpArray(object $opArray): void if ($this->ptrValue($opArray, 'static_variables') !== 0) { $address = $this->unPtr($opArray, 'static_variables'); - $this->unserializeHash(Core::pointerAtAddress('zend_array *', $address), $this->unserializeZval(...)); + $this->unserializeHash(Core::pointerAtAddress(HashTableStruct::class, $address), $this->unserializeZval(...)); } if ($this->ptrValue($opArray, 'literals') !== 0) { $address = $this->unPtr($opArray, 'literals'); @@ -874,7 +853,7 @@ private function unserializeOpArray(object $opArray): void $count = $this->requireCount((int) $opArray->last_literal, 'op_array last_literal'); $this->requireSpan($address, $count * $zvalSize, 'op_array literals'); for ($i = 0; $i < $count; $i++) { - $this->unserializeZval(Core::pointerAtAddress('zval *', $address + $i * $zvalSize)); + $this->unserializeZval(Core::pointerAtAddress(zval::class, $address + $i * $zvalSize)); } } // opcodes: only the array pointer is relocated. Per-opline operands are @@ -897,7 +876,7 @@ private function unserializeOpArray(object $opArray): void $this->requireSpan($defsAddress, $count * PHP_INT_SIZE, 'dynamic_func_defs table'); for ($i = 0; $i < $count; $i++) { $defAddress = $this->unPtrAt($defsAddress + $i * PHP_INT_SIZE); - $this->unserializeOpArray(Core::pointerAtAddress('zend_op_array *', $defAddress)); + $this->unserializeOpArray(Core::pointerAtAddress(zend_op_array::class, $defAddress)); } } $this->unStr($opArray, 'function_name'); @@ -909,12 +888,10 @@ private function unserializeOpArray(object $opArray): void $this->unPtr($opArray, 'prototype'); $this->unPtr($opArray, 'prop_info'); } - /** - * @param \FFI\CData $opArray - */ private function serializeOpArray(object $opArray): void { + /** @var zend_op_array $opArray Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->isSerialized($this->ptrValue($opArray, 'opcodes'))) { return; } @@ -944,13 +921,13 @@ private function serializeOpArray(object $opArray): void if ($this->ptrValue($opArray, 'static_variables') !== 0) { $address = $this->serPtr($opArray, 'static_variables'); - $this->serializeHash(Core::pointerAtAddress('zend_array *', $address), $this->serializeZval(...)); + $this->serializeHash(Core::pointerAtAddress(HashTableStruct::class, $address), $this->serializeZval(...)); } if ($this->ptrValue($opArray, 'literals') !== 0) { $address = $this->serPtr($opArray, 'literals'); $zvalSize = Core::sizeOfType(zval::class); for ($i = 0; $i < $opArray->last_literal; $i++) { - $this->serializeZval(Core::pointerAtAddress('zval *', $address + $i * $zvalSize)); + $this->serializeZval(Core::pointerAtAddress(zval::class, $address + $i * $zvalSize)); } } $this->serPtr($opArray, 'opcodes'); @@ -962,7 +939,7 @@ private function serializeOpArray(object $opArray): void $defsAddress = $this->serPtr($opArray, 'dynamic_func_defs'); for ($i = 0; $i < $opArray->num_dynamic_func_defs; $i++) { $defAddress = $this->serPtrAt($defsAddress + $i * PHP_INT_SIZE); - $this->serializeOpArray(Core::pointerAtAddress('zend_op_array *', $defAddress)); + $this->serializeOpArray(Core::pointerAtAddress(zend_op_array::class, $defAddress)); } } $this->serStr($opArray, 'function_name'); @@ -978,10 +955,10 @@ private function serializeOpArray(object $opArray): void /** * @return array{int, int} [start index, end index) for the arg_info walk - * @param \FFI\CData $opArray */ private function argInfoBounds(object $opArray): array { + /** @var zend_op_array $opArray Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ $count = $this->requireCount((int) $opArray->num_args, 'op_array num_args'); $start = 0; if (($opArray->fn_flags & self::ZEND_ACC_HAS_RETURN_TYPE) !== 0) { @@ -993,12 +970,10 @@ private function argInfoBounds(object $opArray): array return [$start, $count]; } - /** - * @param \FFI\CData $opArray - */ private function unserializeArgInfo(object $opArray): void { + /** @var zend_op_array $opArray Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($opArray, 'arg_info') === 0) { return; } @@ -1008,19 +983,17 @@ private function unserializeArgInfo(object $opArray): void // The array starts at arg_info[start] (start is -1 for a return type) $this->requireSpan($address + $start * $argInfoSize, ($end - $start) * $argInfoSize, 'op_array arg_info'); for ($i = $start; $i < $end; $i++) { - $arg = Core::pointerAtAddress('zend_arg_info *', $address + $i * $argInfoSize); + $arg = Core::pointerAtAddress(zend_arg_info::class, $address + $i * $argInfoSize); if (!$this->isUnserialized($this->ptrValue($arg, 'name'))) { $this->unStr($arg, 'name'); } - $this->unserializeType($arg, 'type'); + $this->unserializeTypeStruct($arg->type); } } - /** - * @param \FFI\CData $opArray - */ private function serializeArgInfo(object $opArray): void { + /** @var zend_op_array $opArray Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($opArray, 'arg_info') === 0) { return; } @@ -1028,19 +1001,17 @@ private function serializeArgInfo(object $opArray): void $argInfoSize = Core::sizeOfType(zend_arg_info::class); [$start, $end] = $this->argInfoBounds($opArray); for ($i = $start; $i < $end; $i++) { - $arg = Core::pointerAtAddress('zend_arg_info *', $address + $i * $argInfoSize); + $arg = Core::pointerAtAddress(zend_arg_info::class, $address + $i * $argInfoSize); if (!$this->isSerialized($this->ptrValue($arg, 'name'))) { $this->serStr($arg, 'name'); } - $this->serializeType($arg, 'type'); + $this->serializeTypeStruct($arg->type); } } - /** - * @param \FFI\CData $opArray - */ private function unserializeVars(object $opArray): void { + /** @var zend_op_array $opArray Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($opArray, 'vars') === 0) { return; } @@ -1050,22 +1021,20 @@ private function unserializeVars(object $opArray): void for ($i = 0; $i < $count; $i++) { $slot = Core::pointerAtAddress('zend_string **', $address + $i * PHP_INT_SIZE); $view = Core::cast('uintptr_t *', $slot); - if (!$this->isUnserialized((int) $view[0]) && (int) $view[0] !== 0) { - $this->requireStringOffset((int) $view[0], 'op_array var name'); - if (((int) $view[0] & 1) !== 0) { - $view[0] = $this->strSectionBase + ((int) $view[0] & ~1); + if (!$this->isUnserialized($this->readSlot($view)) && $this->readSlot($view) !== 0) { + $this->requireStringOffset($this->readSlot($view), 'op_array var name'); + if (($this->readSlot($view) & 1) !== 0) { + $view[0] = $this->strSectionBase + ($this->readSlot($view) & ~1); } else { - $view[0] = $this->base + (int) $view[0]; + $view[0] = $this->base + $this->readSlot($view); } } } } - /** - * @param \FFI\CData $opArray - */ private function serializeVars(object $opArray): void { + /** @var zend_op_array $opArray Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($opArray, 'vars') === 0) { return; } @@ -1073,7 +1042,7 @@ private function serializeVars(object $opArray): void for ($i = 0; $i < $opArray->last_var; $i++) { $slot = Core::pointerAtAddress('zend_string **', $address + $i * PHP_INT_SIZE); $view = Core::cast('uintptr_t *', $slot); - $stored = (int) $view[0]; + $stored = $this->readSlot($view); if ($stored === 0 || $this->isSerialized($stored)) { continue; } @@ -1086,13 +1055,11 @@ private function serializeVars(object $opArray): void } // --- classes ----------------------------------------------------------- - /** - * @param \FFI\CData $zval - */ private function unserializeClass(object $zval): void { - $ce = Core::pointerAtAddress('zend_class_entry *', $this->unPtr($zval->value, 'ce')); + /** @var zval $zval Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ + $ce = Core::pointerAtAddress(zend_class_entry::class, $this->unPtr($zval->value, 'ce')); $this->unStr($ce, 'name'); if ($this->ptrValue($ce, 'parent') !== 0) { if (($ce->ce_flags & self::ZEND_ACC_LINKED) === 0) { @@ -1124,13 +1091,11 @@ private function unserializeClass(object $zval): void $this->unserializeIteratorFuncs($ce); // MAP_PTR / default_object_handlers / get_iterator are execution-only (skipped) } - /** - * @param \FFI\CData $zval - */ private function serializeClass(object $zval): void { - $ce = Core::pointerAtAddress('zend_class_entry *', $this->serPtr($zval->value, 'ce')); + /** @var zval $zval Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ + $ce = Core::pointerAtAddress(zend_class_entry::class, $this->serPtr($zval->value, 'ce')); $this->serStr($ce, 'name'); if ($this->ptrValue($ce, 'parent') !== 0) { if (($ce->ce_flags & self::ZEND_ACC_LINKED) === 0) { @@ -1167,12 +1132,10 @@ private function serializeClass(object $zval): void '__serialize', '__unserialize', '__isset', '__unset', '__tostring', '__callstatic', '__debugInfo', ]; - /** - * @param \FFI\CData $ce - */ private function unserializePropertyTable(object $ce, string $field, int $count): void { + /** @var zend_class_entry $ce Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($ce, $field) === 0) { return; } @@ -1181,30 +1144,26 @@ private function unserializePropertyTable(object $ce, string $field, int $count) $count = $this->requireCount($count, "class {$field} count"); $this->requireSpan($address, $count * $zvalSize, "class {$field}"); for ($i = 0; $i < $count; $i++) { - $this->unserializeZval(Core::pointerAtAddress('zval *', $address + $i * $zvalSize)); + $this->unserializeZval(Core::pointerAtAddress(zval::class, $address + $i * $zvalSize)); } } - /** - * @param \FFI\CData $ce - */ private function serializePropertyTable(object $ce, string $field, int $count): void { + /** @var zend_class_entry $ce Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($ce, $field) === 0) { return; } $address = $this->serPtr($ce, $field); $zvalSize = Core::sizeOfType(zval::class); for ($i = 0; $i < $count; $i++) { - $this->serializeZval(Core::pointerAtAddress('zval *', $address + $i * $zvalSize)); + $this->serializeZval(Core::pointerAtAddress(zval::class, $address + $i * $zvalSize)); } } - /** - * @param \FFI\CData $ce - */ private function unserializePropInfoTable(object $ce): void { + /** @var zend_class_entry $ce Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($ce, 'properties_info_table') === 0) { return; } @@ -1213,68 +1172,60 @@ private function unserializePropInfoTable(object $ce): void $this->requireSpan($address, $count * PHP_INT_SIZE, 'properties_info_table'); for ($i = 0; $i < $count; $i++) { $slot = Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $address + $i * PHP_INT_SIZE)); - if ((int) $slot[0] !== 0) { - $this->requireOffset((int) $slot[0], 'properties_info_table entry'); - $slot[0] = $this->base + (int) $slot[0]; + if ($this->readSlot($slot) !== 0) { + $this->requireOffset($this->readSlot($slot), 'properties_info_table entry'); + $slot[0] = $this->base + $this->readSlot($slot); } } } - /** - * @param \FFI\CData $ce - */ private function serializePropInfoTable(object $ce): void { + /** @var zend_class_entry $ce Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($ce, 'properties_info_table') === 0) { return; } $address = $this->serPtr($ce, 'properties_info_table'); for ($i = 0; $i < $ce->default_properties_count; $i++) { $slot = Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $address + $i * PHP_INT_SIZE)); - $stored = (int) $slot[0]; + $stored = $this->readSlot($slot); if ($stored !== 0) { $slot[0] = $stored - $this->base; } } } - /** - * @param \FFI\CData $ce - */ private function unserializeClassNames(object $ce, string $field, int $count): void { + /** @var zend_class_entry $ce Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ $address = $this->unPtr($ce, $field); $nameSize = Core::sizeOfType(zend_class_name::class); $count = $this->requireCount($count, "class {$field} count"); $this->requireSpan($address, $count * $nameSize, "class {$field}"); for ($i = 0; $i < $count; $i++) { - $name = Core::pointerAtAddress('zend_class_name *', $address + $i * $nameSize); + $name = Core::pointerAtAddress(zend_class_name::class, $address + $i * $nameSize); $this->unStr($name, 'name'); $this->unStr($name, 'lc_name'); } } - /** - * @param \FFI\CData $ce - */ private function serializeClassNames(object $ce, string $field, int $count): void { + /** @var zend_class_entry $ce Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ $address = $this->serPtr($ce, $field); $nameSize = Core::sizeOfType(zend_class_name::class); for ($i = 0; $i < $count; $i++) { - $name = Core::pointerAtAddress('zend_class_name *', $address + $i * $nameSize); + $name = Core::pointerAtAddress(zend_class_name::class, $address + $i * $nameSize); $this->serStr($name, 'name'); $this->serStr($name, 'lc_name'); } } // --- traits (the num_traits branch of zend_file_cache_(un)serialize_class) - /** - * @param \FFI\CData $ce - */ private function unserializeTraitAliases(object $ce): void { + /** @var zend_class_entry $ce Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($ce, 'trait_aliases') === 0) { return; } @@ -1283,7 +1234,7 @@ private function unserializeTraitAliases(object $ce): void // Bound the terminator scan: each slot read must stay inside the region $this->requireSpan($slotAddress, PHP_INT_SIZE, 'trait_aliases array'); while (($aliasAddress = $this->unPtrAt($slotAddress)) !== 0) { - $alias = Core::pointerAtAddress('zend_trait_alias *', $aliasAddress); + $alias = Core::pointerAtAddress(zend_trait_alias::class, $aliasAddress); $this->unStr($alias->trait_method, 'method_name'); $this->unStr($alias->trait_method, 'class_name'); $this->unStr($alias, 'alias'); @@ -1291,30 +1242,26 @@ private function unserializeTraitAliases(object $ce): void $this->requireSpan($slotAddress, PHP_INT_SIZE, 'trait_aliases array'); } } - /** - * @param \FFI\CData $ce - */ private function serializeTraitAliases(object $ce): void { + /** @var zend_class_entry $ce Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($ce, 'trait_aliases') === 0) { return; } $slotAddress = $this->serPtr($ce, 'trait_aliases'); while (($aliasAddress = $this->serPtrAt($slotAddress)) !== 0) { - $alias = Core::pointerAtAddress('zend_trait_alias *', $aliasAddress); + $alias = Core::pointerAtAddress(zend_trait_alias::class, $aliasAddress); $this->serStr($alias->trait_method, 'method_name'); $this->serStr($alias->trait_method, 'class_name'); $this->serStr($alias, 'alias'); $slotAddress += PHP_INT_SIZE; } } - /** - * @param \FFI\CData $ce - */ private function unserializeTraitPrecedences(object $ce): void { + /** @var zend_class_entry $ce Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($ce, 'trait_precedences') === 0) { return; } @@ -1322,7 +1269,7 @@ private function unserializeTraitPrecedences(object $ce): void $slotAddress = $this->unPtr($ce, 'trait_precedences'); $this->requireSpan($slotAddress, PHP_INT_SIZE, 'trait_precedences array'); while (($precedenceAddress = $this->unPtrAt($slotAddress)) !== 0) { - $precedence = Core::pointerAtAddress('zend_trait_precedence *', $precedenceAddress); + $precedence = Core::pointerAtAddress(zend_trait_precedence::class, $precedenceAddress); $this->unStr($precedence->trait_method, 'method_name'); $this->unStr($precedence->trait_method, 'class_name'); $excludeBase = Core::addressOf($precedence->exclude_class_names); @@ -1335,18 +1282,16 @@ private function unserializeTraitPrecedences(object $ce): void $this->requireSpan($slotAddress, PHP_INT_SIZE, 'trait_precedences array'); } } - /** - * @param \FFI\CData $ce - */ private function serializeTraitPrecedences(object $ce): void { + /** @var zend_class_entry $ce Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($ce, 'trait_precedences') === 0) { return; } $slotAddress = $this->serPtr($ce, 'trait_precedences'); while (($precedenceAddress = $this->serPtrAt($slotAddress)) !== 0) { - $precedence = Core::pointerAtAddress('zend_trait_precedence *', $precedenceAddress); + $precedence = Core::pointerAtAddress(zend_trait_precedence::class, $precedenceAddress); $this->serStr($precedence->trait_method, 'method_name'); $this->serStr($precedence->trait_method, 'class_name'); $excludeBase = Core::addressOf($precedence->exclude_class_names); @@ -1356,16 +1301,14 @@ private function serializeTraitPrecedences(object $ce): void $slotAddress += PHP_INT_SIZE; } } - /** - * @param \FFI\CData $zval - */ private function unserializePropInfo(object $zval): void { + /** @var zval $zval Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->isUnserialized($this->ptrValue($zval->value, 'ptr'))) { return; } - $prop = Core::pointerAtAddress('zend_property_info *', $this->unPtr($zval->value, 'ptr')); + $prop = Core::pointerAtAddress(zend_property_info::class, $this->unPtr($zval->value, 'ptr')); if ($this->isUnserialized($this->ptrValue($prop, 'ce'))) { return; } @@ -1384,22 +1327,20 @@ private function unserializePropInfo(object $zval): void for ($i = 0; $i < self::PROPERTY_HOOK_COUNT; $i++) { $hookAddress = $this->unPtrAt($hooksAddress + $i * PHP_INT_SIZE); if ($hookAddress !== 0) { - $this->unserializeOpArray(Core::pointerAtAddress('zend_function *', $hookAddress)->op_array); + $this->unserializeOpArray(Core::pointerAtAddress(zend_function::class, $hookAddress)->op_array); } } } - $this->unserializeType($prop, 'type'); + $this->unserializeTypeStruct($prop->type); } - /** - * @param \FFI\CData $zval - */ private function serializePropInfo(object $zval): void { + /** @var zval $zval Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->isSerialized($this->ptrValue($zval->value, 'ptr'))) { return; } - $prop = Core::pointerAtAddress('zend_property_info *', $this->serPtr($zval->value, 'ptr')); + $prop = Core::pointerAtAddress(zend_property_info::class, $this->serPtr($zval->value, 'ptr')); if ($this->isSerialized($this->ptrValue($prop, 'ce'))) { return; } @@ -1417,22 +1358,20 @@ private function serializePropInfo(object $zval): void for ($i = 0; $i < self::PROPERTY_HOOK_COUNT; $i++) { $hookAddress = $this->serPtrAt($hooksAddress + $i * PHP_INT_SIZE); if ($hookAddress !== 0) { - $this->serializeOpArray(Core::pointerAtAddress('zend_function *', $hookAddress)->op_array); + $this->serializeOpArray(Core::pointerAtAddress(zend_function::class, $hookAddress)->op_array); } } } - $this->serializeType($prop, 'type'); + $this->serializeTypeStruct($prop->type); } - /** - * @param \FFI\CData $zval - */ private function unserializeClassConstant(object $zval): void { + /** @var zval $zval Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->isUnserialized($this->ptrValue($zval->value, 'ptr'))) { return; } - $constant = Core::pointerAtAddress('zend_class_constant *', $this->unPtr($zval->value, 'ptr')); + $constant = Core::pointerAtAddress(zend_class_constant::class, $this->unPtr($zval->value, 'ptr')); if ($this->isUnserialized($this->ptrValue($constant, 'ce'))) { return; } @@ -1442,18 +1381,16 @@ private function unserializeClassConstant(object $zval): void $this->unStr($constant, 'doc_comment'); } $this->unserializeAttributes($constant, 'attributes'); - $this->unserializeType($constant, 'type'); + $this->unserializeTypeStruct($constant->type); } - /** - * @param \FFI\CData $zval - */ private function serializeClassConstant(object $zval): void { + /** @var zval $zval Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->isSerialized($this->ptrValue($zval->value, 'ptr'))) { return; } - $constant = Core::pointerAtAddress('zend_class_constant *', $this->serPtr($zval->value, 'ptr')); + $constant = Core::pointerAtAddress(zend_class_constant::class, $this->serPtr($zval->value, 'ptr')); if ($this->isSerialized($this->ptrValue($constant, 'ce'))) { return; } @@ -1463,11 +1400,8 @@ private function serializeClassConstant(object $zval): void $this->serStr($constant, 'doc_comment'); } $this->serializeAttributes($constant, 'attributes'); - $this->serializeType($constant, 'type'); + $this->serializeTypeStruct($constant->type); } - /** - * @param \FFI\CData $ce - */ /** * zf_* field order matches the C walk (zend_file_cache.c), not the struct layout. @@ -1484,36 +1418,34 @@ private function serializeClassConstant(object $zval): void * the placeholder is preserved verbatim like every other execution-only field * and the written file keeps the exact bytes the engine expects. * - * @param \FFI\CData $ce */ private function unserializeIteratorFuncs(object $ce): void { + /** @var zend_class_entry $ce Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($ce, 'iterator_funcs_ptr') !== 0) { $address = $this->unPtr($ce, 'iterator_funcs_ptr'); - $funcs = Core::pointerAtAddress('zend_class_iterator_funcs *', $address); + $funcs = Core::pointerAtAddress(zend_class_iterator_funcs::class, $address); foreach (self::ITERATOR_FUNC_FIELDS as $field) { $this->unPtr($funcs, $field); } } if ($this->ptrValue($ce, 'arrayaccess_funcs_ptr') !== 0) { $address = $this->unPtr($ce, 'arrayaccess_funcs_ptr'); - $funcs = Core::pointerAtAddress('zend_class_arrayaccess_funcs *', $address); + $funcs = Core::pointerAtAddress(zend_class_arrayaccess_funcs::class, $address); foreach (self::ARRAYACCESS_FUNC_FIELDS as $field) { $this->unPtr($funcs, $field); } } } - /** - * @param \FFI\CData $ce - */ private function serializeIteratorFuncs(object $ce): void { + /** @var zend_class_entry $ce Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ // The C serialize converts the zf_* members through the still-real struct // pointer first and the struct pointer itself last; mirrored exactly $iteratorAddress = $this->ptrValue($ce, 'iterator_funcs_ptr'); if ($iteratorAddress !== 0) { - $funcs = Core::pointerAtAddress('zend_class_iterator_funcs *', $iteratorAddress); + $funcs = Core::pointerAtAddress(zend_class_iterator_funcs::class, $iteratorAddress); foreach (self::ITERATOR_FUNC_FIELDS as $field) { $this->serPtr($funcs, $field); } @@ -1521,7 +1453,7 @@ private function serializeIteratorFuncs(object $ce): void } $arrayAccessAddress = $this->ptrValue($ce, 'arrayaccess_funcs_ptr'); if ($arrayAccessAddress !== 0) { - $funcs = Core::pointerAtAddress('zend_class_arrayaccess_funcs *', $arrayAccessAddress); + $funcs = Core::pointerAtAddress(zend_class_arrayaccess_funcs::class, $arrayAccessAddress); foreach (self::ARRAYACCESS_FUNC_FIELDS as $field) { $this->serPtr($funcs, $field); } @@ -1530,12 +1462,10 @@ private function serializeIteratorFuncs(object $ce): void } // --- warnings / early bindings ----------------------------------------- - /** - * @param \FFI\CData $script - */ private function unserializeWarnings(object $script): void { + /** @var zend_persistent_script $script Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($script, 'warnings') === 0) { return; } @@ -1543,39 +1473,37 @@ private function unserializeWarnings(object $script): void $count = $this->requireCount((int) $script->num_warnings, 'num_warnings'); $this->requireSpan($address, $count * PHP_INT_SIZE, 'warnings table'); for ($i = 0; $i < $count; $i++) { - $slot = Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $address + $i * PHP_INT_SIZE)); - $this->requireOffset((int) $slot[0], 'warning entry'); - $slot[0] = $this->base + (int) $slot[0]; - $warning = Core::pointerAtAddress('zend_error_info *', (int) $slot[0]); + $slot = Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $address + $i * PHP_INT_SIZE)); + $stored = $this->readSlot($slot); + $this->requireOffset($stored, 'warning entry'); + $resolved = $this->base + $stored; + $slot[0] = $resolved; + $warning = Core::pointerAtAddress(zend_error_info::class, $resolved); $this->unStr($warning, 'filename'); $this->unStr($warning, 'message'); } } - /** - * @param \FFI\CData $script - */ private function serializeWarnings(object $script): void { + /** @var zend_persistent_script $script Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($script, 'warnings') === 0) { return; } $address = $this->serPtr($script, 'warnings'); for ($i = 0; $i < $script->num_warnings; $i++) { $slot = Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $address + $i * PHP_INT_SIZE)); - $warnAddr = (int) $slot[0]; + $warnAddr = $this->readSlot($slot); $slot[0] = $warnAddr - $this->base; - $warning = Core::pointerAtAddress('zend_error_info *', $warnAddr); + $warning = Core::pointerAtAddress(zend_error_info::class, $warnAddr); $this->serStr($warning, 'filename'); $this->serStr($warning, 'message'); } } - /** - * @param \FFI\CData $script - */ private function unserializeEarlyBindings(object $script): void { + /** @var zend_persistent_script $script Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($script, 'early_bindings') === 0) { return; } @@ -1584,25 +1512,23 @@ private function unserializeEarlyBindings(object $script): void $count = $this->requireCount((int) $script->num_early_bindings, 'num_early_bindings'); $this->requireSpan($address, $count * $bindingSize, 'early_bindings table'); for ($i = 0; $i < $count; $i++) { - $binding = Core::pointerAtAddress('zend_early_binding *', $address + $i * $bindingSize); + $binding = Core::pointerAtAddress(zend_early_binding::class, $address + $i * $bindingSize); $this->unStr($binding, 'lcname'); $this->unStr($binding, 'rtd_key'); $this->unStr($binding, 'lc_parent_name'); } } - /** - * @param \FFI\CData $script - */ private function serializeEarlyBindings(object $script): void { + /** @var zend_persistent_script $script Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if ($this->ptrValue($script, 'early_bindings') === 0) { return; } $address = $this->serPtr($script, 'early_bindings'); $bindingSize = Core::sizeOfType(zend_early_binding::class); for ($i = 0; $i < $script->num_early_bindings; $i++) { - $binding = Core::pointerAtAddress('zend_early_binding *', $address + $i * $bindingSize); + $binding = Core::pointerAtAddress(zend_early_binding::class, $address + $i * $bindingSize); $this->serStr($binding, 'lcname'); $this->serStr($binding, 'rtd_key'); $this->serStr($binding, 'lc_parent_name'); diff --git a/src/OpCache/ReflectionOpcacheFile.php b/src/OpCache/ReflectionOpcacheFile.php index 510988d..1ce6154 100644 --- a/src/OpCache/ReflectionOpcacheFile.php +++ b/src/OpCache/ReflectionOpcacheFile.php @@ -14,9 +14,13 @@ namespace ZEngine\OpCache; use FFI; -use FFI\CData; use ZEngine\Core; use ZEngine\Generated\Bucket; +use ZEngine\Generated\HashTable as HashTableStruct; +use ZEngine\Generated\zend_class_entry; +use ZEngine\Generated\zend_op_array; +use ZEngine\Generated\zend_persistent_script; +use ZEngine\Generated\zend_string; use ZEngine\Reflection\ReflectionClass; use ZEngine\Reflection\ReflectionFunction; use ZEngine\Type\HashTable; @@ -44,14 +48,15 @@ final class ReflectionOpcacheFile private array $donors = []; /** - * @param \FFI\CData $script Relocated zend_persistent_script inside the image buffer - * @param object|null $imageOwner Owner of the relocated buffer (the PayloadRelocator): - * retained so that holding this view alone provably keeps - * the buffer - which every wrapper this view hands out - * points into - alive (see CacheImageSync, whose swapped-in - * bodies keep executing out of that buffer) + * @param \FFI\CData|zend_persistent_script $script Relocated zend_persistent_script inside the image buffer + * @param object|null $imageOwner Owner of the relocated buffer (the PayloadRelocator): + * retained so that holding this view alone provably keeps + * the buffer - which every wrapper this view hands out + * points into - alive (see CacheImageSync, whose swapped-in + * bodies keep executing out of that buffer) */ public function __construct( + /** @var zend_persistent_script Typed view of the relocated persistent script this handle wraps */ private readonly object $script, // @phpstan-ignore property.onlyWritten (lifetime pin: held, never read) private readonly ?object $imageOwner = null, @@ -61,7 +66,7 @@ public function __construct( * The relocated zend_persistent_script this handle wraps * * @internal core-layer escape hatch for BinaryCacheFile/ScriptSerializer - * @return \FFI\CData + * @return zend_persistent_script */ public function getRawScript(): object { @@ -90,7 +95,11 @@ public function donorImages(): array */ public function getFileName(): string { - return StringEntry::fromCData($this->script->script->filename)->getStringValue(); + // A persistent script always carries a filename block + $filename = $this->script->script->filename; + \assert($filename !== null); + + return StringEntry::fromCData($filename)->getStringValue(); } /** @@ -98,7 +107,7 @@ public function getFileName(): string */ public function getScriptFunction(): ReflectionFunction { - $function = Core::cast('zend_function *', FFI::addr($this->script->script->main_op_array)); + $function = Core::cast('zend_function *', Core::addr($this->script->script->main_op_array)); return ReflectionFunction::fromCData($function); } @@ -108,7 +117,7 @@ public function getScriptFunction(): ReflectionFunction */ public function functionTable(): HashTable { - return HashTable::fromCData(FFI::addr($this->script->script->function_table)); + return HashTable::fromCData(Core::addr($this->script->script->function_table)); } /** @@ -116,7 +125,7 @@ public function functionTable(): HashTable */ public function classTable(): HashTable { - return HashTable::fromCData(FFI::addr($this->script->script->class_table)); + return HashTable::fromCData(Core::addr($this->script->script->class_table)); } /** @@ -165,7 +174,12 @@ public function addMethodFrom(self $donor, string $donorClassName, string $metho [$keyAddress, $methodAddress] = $entry; // Re-point the method's scope at the adopting class - $method = Core::pointerAtAddress('zend_op_array *', $methodAddress); + $method = Core::pointerAtAddress(zend_op_array::class, $methodAddress); + // A grafted method op_array always carries its donor-class scope slot + \assert($method->scope !== null); + // FFI::addr must stay inline on the pointer-field access to yield the + // scope SLOT address (a by-value hop would address a pointer copy). + // @phpstan-ignore argument.type (FFI::addr of a zend_class_entry* pointer field) Core::cast('uintptr_t *', FFI::addr($method->scope))[0] = Core::addressOf($targetClass); $this->insertPtrEntry($targetClass->function_table, $keyAddress, $methodAddress); @@ -217,22 +231,27 @@ public function getClasses(): array * Finds a class entry by its own name, case-insensitively; class-table * bucket keys can be opcache rtd keys, so match on ce->name instead. * - * @return \FFI\CData|null a zend_class_entry* into the image + * @return zend_class_entry|null a zend_class_entry* into the image */ private function findClassByName(string $className): ?object { - $ht = $this->script->script->class_table; - $bucketSize = Core::sizeOfType(Bucket::class); + $ht = $this->script->script->class_table; + $bucketSize = Core::sizeOfType(Bucket::class); + // A populated class table always carries a bucket data block + \assert($ht->arData !== null); $dataAddress = Core::addressOf($ht->arData); for ($i = 0; $i < $ht->nNumUsed; $i++) { - $bucket = Core::pointerAtAddress('Bucket *', $dataAddress + $i * $bucketSize); + $bucket = Core::pointerAtAddress(Bucket::class, $dataAddress + $i * $bucketSize); if ($bucket->val->u1->v->type === 0) { continue; } $classEntry = Core::pointerAtAddress( - 'zend_class_entry *', - (int) Core::cast('uintptr_t *', FFI::addr($bucket->val->value))[0], + zend_class_entry::class, + // IS_PTR bucket: the stored class-entry pointer lives in the value union's long slot + $bucket->val->value->lval, ); + // Every compiled class entry carries its own name block + \assert($classEntry->name !== null); $name = StringEntry::fromCData($classEntry->name)->getStringValue(); if (strcasecmp($name, $className) === 0) { return $classEntry; @@ -245,7 +264,7 @@ private function findClassByName(string $className): ?object /** * Finds a bucket by exact key in a keyed image table. * - * @param \FFI\CData $ht HashTable view + * @param HashTableStruct $ht HashTable view * @return array{int, int}|null [key zend_string address, value pointer address] */ private static function findKeyedEntry(object $ht, string $key): ?array @@ -253,17 +272,20 @@ private static function findKeyedEntry(object $ht, string $key): ?array if (($ht->u->flags & Core::engineConstant('HASH_FLAG_UNINITIALIZED')) !== 0) { return null; } - $bucketSize = Core::sizeOfType(Bucket::class); + $bucketSize = Core::sizeOfType(Bucket::class); + // An initialized (non-uninitialized) table always carries a bucket data block + \assert($ht->arData !== null); $dataAddress = Core::addressOf($ht->arData); for ($i = 0; $i < $ht->nNumUsed; $i++) { - $bucket = Core::pointerAtAddress('Bucket *', $dataAddress + $i * $bucketSize); + $bucket = Core::pointerAtAddress(Bucket::class, $dataAddress + $i * $bucketSize); if ($bucket->val->u1->v->type === 0 || $bucket->key === null) { continue; } if (StringEntry::fromCData($bucket->key)->getStringValue() === $key) { return [ Core::addressOf($bucket->key), - (int) Core::cast('uintptr_t *', FFI::addr($bucket->val->value))[0], + // IS_PTR bucket: the stored pointer lives in the value union's long slot + $bucket->val->value->lval, ]; } } @@ -279,7 +301,7 @@ private static function findKeyedEntry(object $ht, string $key): ?array * the persisted format expects it (hash slots ahead of arData, bucket-index * chains via Z_NEXT, HT_SIZE_TO_MASK = -(2 * nTableSize)). * - * @param \FFI\CData $ht HashTable view (embedded in the image) + * @param HashTableStruct $ht HashTable view (embedded in the image) */ private function insertPtrEntry(object $ht, int $keyAddress, int $valueAddress): void { @@ -287,7 +309,7 @@ private function insertPtrEntry(object $ht, int $keyAddress, int $valueAddress): if (($flags & Core::engineConstant('HASH_FLAG_PACKED')) !== 0) { throw OpCacheException::unsupportedPayload('grafting into a packed hashtable'); } - $key = Core::pointerAtAddress('zend_string *', $keyAddress); + $key = Core::pointerAtAddress(zend_string::class, $keyAddress); $hash = $key->h; if ($hash === 0) { throw OpCacheException::unsupportedPayload('graft key string carries no precomputed hash'); @@ -295,9 +317,11 @@ private function insertPtrEntry(object $ht, int $keyAddress, int $valueAddress): $bucketSize = Core::sizeOfType(Bucket::class); $uninitialized = ($flags & Core::engineConstant('HASH_FLAG_UNINITIALIZED')) !== 0; - $used = $uninitialized ? 0 : $ht->nNumUsed; - $tableSize = $uninitialized ? 8 : $ht->nTableSize; - $oldData = $uninitialized ? 0 : Core::addressOf($ht->arData); + // An initialized image table always points its data block at real buckets + \assert($uninitialized || $ht->arData !== null); + $used = $uninitialized ? 0 : $ht->nNumUsed; + $tableSize = $uninitialized ? 8 : $ht->nTableSize; + $oldData = $uninitialized ? 0 : Core::addressOf($ht->arData); if (!$uninitialized && self::findKeyedEntry($ht, StringEntry::fromCData($key)->getStringValue()) !== null) { throw OpCacheException::duplicateHashTableKey(StringEntry::fromCData($key)->getStringValue()); @@ -316,7 +340,7 @@ private function insertPtrEntry(object $ht, int $keyAddress, int $valueAddress): $newData = $blockBase + $hashBytes; if ($used > 0) { - FFI::memcpy( + Core::memcpy( Core::cast('char *', Core::pointerAtAddress('void *', $newData)), Core::cast('char *', Core::pointerAtAddress('void *', $oldData)), $used * $bucketSize, @@ -324,29 +348,32 @@ private function insertPtrEntry(object $ht, int $keyAddress, int $valueAddress): } // The appended bucket: an IS_PTR zval, hash and key - $bucket = Core::pointerAtAddress('Bucket *', $newData + $used * $bucketSize); - $bucket->val->u1->type_info = Core::engineConstant('IS_PTR'); - Core::cast('uintptr_t *', FFI::addr($bucket->val->value))[0] = $valueAddress; - $bucket->h = $hash; - $bucket->key = $key; + $bucket = Core::pointerAtAddress(Bucket::class, $newData + $used * $bucketSize); + $bucket->val->u1->type_info = Core::engineConstant('IS_PTR'); + Core::cast('uintptr_t *', Core::addr($bucket->val->value))[0] = $valueAddress; + $bucket->h = $hash; + $bucket->key = $key; // HT_HASH_RESET + full rehash (bucket-index chains, like zend_hash_persist) for ($i = 0; $i < 2 * $tableSize; $i++) { Core::cast('uint32_t *', Core::pointerAtAddress('void *', $blockBase + $i * 4))[0] = 0xFFFFFFFF; // HT_INVALID_IDX } for ($idx = 0; $idx < $newUsed; $idx++) { - $entry = Core::pointerAtAddress('Bucket *', $newData + $idx * $bucketSize); + $entry = Core::pointerAtAddress(Bucket::class, $newData + $idx * $bucketSize); if ($entry->val->u1->v->type === 0) { continue; } - $nIndex = ($entry->h | $newMask) & 0xFFFFFFFF; - $slot = $nIndex - 0x100000000; // (int32_t)nIndex, always negative - $slotAddr = $newData + $slot * 4; - $entry->val->u2->next = (int) Core::cast('uint32_t *', Core::pointerAtAddress('void *', $slotAddr))[0]; + $nIndex = ($entry->h | $newMask) & 0xFFFFFFFF; + $slot = $nIndex - 0x100000000; // (int32_t)nIndex, always negative + $slotAddr = $newData + $slot * 4; + // HT_HASH slots are uint32_t; the deref reads as a PHP int (Z_NEXT chain head) + $chainHead = Core::cast('uint32_t *', Core::pointerAtAddress('void *', $slotAddr))[0]; + \assert(\is_int($chainHead)); + $entry->val->u2->next = $chainHead; Core::cast('uint32_t *', Core::pointerAtAddress('void *', $slotAddr))[0] = $idx; } - $ht->arData = Core::pointerAtAddress('Bucket *', $newData); + $ht->arData = Core::pointerAtAddress(Bucket::class, $newData); $ht->nNumUsed = $newUsed; $ht->nNumOfElements = ($uninitialized ? 0 : $ht->nNumOfElements) + 1; $ht->nTableSize = $tableSize; diff --git a/src/OpCache/ScriptSerializer.php b/src/OpCache/ScriptSerializer.php index fa2e4a7..a9dceb8 100644 --- a/src/OpCache/ScriptSerializer.php +++ b/src/OpCache/ScriptSerializer.php @@ -17,6 +17,7 @@ use FFI\CData; use ZEngine\Core; use ZEngine\Generated\Bucket; +use ZEngine\Generated\HashTable as HashTableStruct; use ZEngine\Generated\zend_arg_info; use ZEngine\Generated\zend_ast; use ZEngine\Generated\zend_ast_list; @@ -139,7 +140,7 @@ final class ScriptSerializer private readonly int $zendStringHeaderSize; /** - * @param CData $script the relocated zend_persistent_script* of the live image + * @param CData|zend_persistent_script $script the relocated zend_persistent_script* of the live image */ public function __construct(private readonly object $script) { @@ -177,6 +178,8 @@ public function serialize(): string $this->persistScript($scriptAddress); $this->resolveDeferred(); + // The emit buffer was just allocated above and is never cleared here + \assert($this->out !== null); // The emitted region is a valid relocated image; the on-disk offset // encoding is the relocator's serialize - byte-tested machinery $meta = CacheMetaInfo::forPayload( @@ -234,7 +237,8 @@ private function unit(int $source, int $size): array FFI::memcpy( Core::cast('char *', Core::pointerAtAddress('void *', $new)), Core::cast('char *', Core::pointerAtAddress('void *', $source)), - $size, + // max(...,0) only states the non-negative unit size to the analyser + max($size, 0), ); return [$new, true]; @@ -266,9 +270,22 @@ private function mapAddress(int $source): int } /** - * Reads a pointer field's stored value as an integer (0 for C NULL). + * Reads a uintptr_t pointer slot as a PHP int - the raw-pointer read + * primitive. The dereferenced CData element is always an integer at runtime; + * the guard states that to the analyser without widening any real value. * - * @param \FFI\CData $owner + * @param \FFI\CData $slot a uintptr_t* view over the slot to read + */ + private function readSlot(object $slot): int + { + $value = $slot[0]; + \assert(\is_int($value)); + + return $value; + } + + /** + * Reads a pointer field's stored value as an integer (0 for C NULL). */ private function ptrValue(object $owner, string $field): int { @@ -276,20 +293,24 @@ private function ptrValue(object $owner, string $field): int return 0; } - return (int) Core::cast('uintptr_t *', FFI::addr($owner->$field))[0]; + // A dynamically-named pointer field cannot be statically resolved, so + // FFI::addr() on the mixed field read is the one irreducible CData hop. + // @phpstan-ignore argument.type (FFI::addr of a dynamic FFI\CData pointer field) + return $this->readSlot(Core::cast('uintptr_t *', FFI::addr($owner->$field))); } /** * Writes a pointer field in the emit pass (no-op while measuring). The * field always holds its non-null source value at this point. * - * @param \FFI\CData $owner a view into the COPY + * @param object $owner a view into the COPY */ private function put(object $owner, string $field, int $address): void { if ($this->phase !== 2) { return; } + // @phpstan-ignore argument.type (FFI::addr of a dynamic FFI\CData pointer field) $slot = Core::cast('uintptr_t *', FFI::addr($owner->$field)); $slot[0] = $address; } @@ -306,7 +327,7 @@ private function putAt(int $slotAddress, int $value): void /** Reads a raw pointer slot */ private function slotValue(int $slotAddress): int { - return (int) Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $slotAddress))[0]; + return $this->readSlot(Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $slotAddress))); } /** Defers a copy-slot rewrite until the xlat table is complete */ @@ -319,13 +340,14 @@ private function deferAt(int $slotAddress, int $sourceTarget, string $what): voi } /** - * @param \FFI\CData $owner a view into the COPY, field currently non-null + * @param object $owner a view into the COPY, field currently non-null */ private function defer(object $owner, string $field, int $sourceTarget, string $what): void { if ($this->phase !== 2) { return; } + // @phpstan-ignore argument.type (FFI::addr of a dynamic FFI\CData pointer field) $this->deferred[] = [Core::addressOf(FFI::addr($owner->$field)), $sourceTarget, $what]; } @@ -346,11 +368,11 @@ private function resolveDeferred(): void */ private function persistString(int $source): int { - $string = Core::pointerAtAddress('zend_string *', $source); + $string = Core::pointerAtAddress(zend_string::class, $source); $size = $this->zendStringHeaderSize + $string->len + 1; [$new, $first] = $this->unit($source, $size); if ($first && $this->phase === 2) { - $copy = Core::pointerAtAddress('zend_string *', $new); + $copy = Core::pointerAtAddress(zend_string::class, $new); $typeInfo = $copy->gc->u->type_info; if (($typeInfo & Core::engineConstant('IS_STR_INTERNED')) === 0) { // zend_set_str_gc_flags, file_cache_only branch @@ -371,11 +393,12 @@ private function persistString(int $source): int * HashTable struct itself lives in its owner (embedded) or in its own unit * (zend_array). $entry receives [source zval address, copy zval address]. * - * @param \FFI\CData $ht source HashTable view - * @param \FFI\CData $htCopy copy HashTable view (same as $ht while measuring) + * @param HashTableStruct $ht source HashTable view + * @param object $htCopy copy HashTable view (same as $ht while measuring) */ private function persistHashData(object $ht, object $htCopy, callable $entry): void { + /** @var HashTableStruct $ht Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ if (($ht->u->flags & Core::engineConstant('HASH_FLAG_UNINITIALIZED')) !== 0) { return; // arData is written as 0 by the relocator's serialize stage } @@ -403,20 +426,20 @@ private function persistHashData(object $ht, object $htCopy, callable $entry): v $sourceEntry = $dataAddress + $i * $entrySize; $copyEntry = $this->phase === 2 ? $newData + $i * $entrySize : $sourceEntry; if ($packed) { - $zv = Core::pointerAtAddress('zval *', $sourceEntry); + $zv = Core::pointerAtAddress(zval::class, $sourceEntry); if ($zv->u1->v->type !== 0) { $entry($sourceEntry, $copyEntry); } continue; } - $bucket = Core::pointerAtAddress('Bucket *', $sourceEntry); + $bucket = Core::pointerAtAddress(Bucket::class, $sourceEntry); if ($bucket->val->u1->v->type === 0) { continue; // hole: bytes copied verbatim, nothing to walk } $keyAddress = $this->ptrValue($bucket, 'key'); if ($keyAddress !== 0) { $newKey = $this->persistString($keyAddress); - $bucketCopy = Core::pointerAtAddress('Bucket *', $copyEntry); + $bucketCopy = Core::pointerAtAddress(Bucket::class, $copyEntry); if ($this->phase === 2) { $this->put($bucketCopy, 'key', $newKey); } @@ -430,8 +453,8 @@ private function persistArray(int $source, callable $entry): int { [$new, $first] = $this->unit($source, Core::sizeOfType('HashTable')); if ($first) { - $ht = Core::pointerAtAddress('HashTable *', $source); - $htCopy = $this->phase === 2 ? Core::pointerAtAddress('HashTable *', $new) : $ht; + $ht = Core::pointerAtAddress(HashTableStruct::class, $source); + $htCopy = $this->phase === 2 ? Core::pointerAtAddress(HashTableStruct::class, $new) : $ht; $this->persistHashData($ht, $htCopy, $entry); } @@ -442,8 +465,8 @@ private function persistArray(int $source, callable $entry): int private function persistZval(int $source, int $copy): void { - $zv = Core::pointerAtAddress('zval *', $source); - $zvCopy = $this->phase === 2 ? Core::pointerAtAddress('zval *', $copy) : $zv; + $zv = Core::pointerAtAddress(zval::class, $source); + $zvCopy = $this->phase === 2 ? Core::pointerAtAddress(zval::class, $copy) : $zv; switch ($zv->u1->v->type) { case self::IS_STRING: $this->put($zvCopy->value, 'str', $this->persistString($this->ptrValue($zv->value, 'str'))); @@ -492,7 +515,7 @@ private function persistAstNode(int $source): int private function persistAstNodeBody(int $source, int $copy): void { - $ast = Core::pointerAtAddress('zend_ast *', $source); + $ast = Core::pointerAtAddress(zend_ast::class, $source); $kind = $ast->kind; if ($kind === self::ZEND_AST_ZVAL || $kind === self::ZEND_AST_CONSTANT) { $valueOffset = Core::sizeOfType('zend_ast_zval') - Core::sizeOfType(zval::class); @@ -525,7 +548,7 @@ private function astChildren(int $source, int $kind): array private function astNodeSize(int $source): int { - $ast = Core::pointerAtAddress('zend_ast *', $source); + $ast = Core::pointerAtAddress(zend_ast::class, $source); $kind = $ast->kind; if ($kind === self::ZEND_AST_ZVAL || $kind === self::ZEND_AST_CONSTANT) { return Core::sizeOfType('zend_ast_zval'); @@ -548,30 +571,30 @@ private function persistAttributes(object $owner, object $ownerCopy, string $fie return; } $new = $this->persistArray($source, function (int $zvalSource, int $zvalCopy): void { - $zv = Core::pointerAtAddress('zval *', $zvalSource); + $zv = Core::pointerAtAddress(zval::class, $zvalSource); $attrSource = $this->ptrValue($zv->value, 'ptr'); - $attr = Core::pointerAtAddress('zend_attribute *', $attrSource); + $attr = Core::pointerAtAddress(zend_attribute::class, $attrSource); $argSize = Core::sizeOfType(zend_attribute_arg::class); // ZEND_ATTRIBUTE_SIZE(argc) $size = Core::sizeOfType(zend_attribute::class) + $argSize * $attr->argc - $argSize; [$new, $first] = $this->unit($attrSource, $size); if ($this->phase === 2) { - $this->put(Core::pointerAtAddress('zval *', $zvalCopy)->value, 'ptr', $new); + $this->put(Core::pointerAtAddress(zval::class, $zvalCopy)->value, 'ptr', $new); } if (!$first) { return; } - $attrCopy = $this->phase === 2 ? Core::pointerAtAddress('zend_attribute *', $new) : $attr; + $attrCopy = $this->phase === 2 ? Core::pointerAtAddress(zend_attribute::class, $new) : $attr; $this->put($attrCopy, 'name', $this->persistString($this->ptrValue($attr, 'name'))); $this->put($attrCopy, 'lcname', $this->persistString($this->ptrValue($attr, 'lcname'))); $argBase = Core::addressOf($attr->args); for ($i = 0; $i < $attr->argc; $i++) { $argSource = $argBase + $i * $argSize; $argCopy = $this->phase === 2 ? Core::addressOf($attrCopy->args) + $i * $argSize : $argSource; - $arg = Core::pointerAtAddress('zend_attribute_arg *', $argSource); + $arg = Core::pointerAtAddress(zend_attribute_arg::class, $argSource); $nameAddr = $this->ptrValue($arg, 'name'); if ($nameAddr !== 0) { - $this->put(Core::pointerAtAddress('zend_attribute_arg *', $argCopy), 'name', $this->persistString($nameAddr)); + $this->put(Core::pointerAtAddress(zend_attribute_arg::class, $argCopy), 'name', $this->persistString($nameAddr)); } $valueOffset = $argSize - Core::sizeOfType(zval::class); $this->persistZval($argSource + $valueOffset, $argCopy + $valueOffset); @@ -583,15 +606,16 @@ private function persistAttributes(object $owner, object $ownerCopy, string $fie // --- types ------------------------------------------------------------------------ /** - * @param \FFI\CData $type source zend_type view (embedded) - * @param \FFI\CData $typeCopy copy zend_type view + * @param zend_type $type source zend_type view (embedded) + * @param object $typeCopy copy zend_type view */ private function persistType(object $type, object $typeCopy): void { + /** @var zend_type $type Narrowed to the stub view at the boundary; the runtime value is FFI\CData */ $typeMask = $type->type_mask; if (($typeMask & self::TYPE_LIST_BIT) !== 0) { $listSource = $this->ptrValue($type, 'ptr'); - $list = Core::pointerAtAddress('zend_type_list *', $listSource); + $list = Core::pointerAtAddress(zend_type_list::class, $listSource); $typeSize = Core::sizeOfType(zend_type::class); $entryBase = Core::sizeOfType(zend_type_list::class) - $typeSize; $size = $entryBase + $typeSize * $list->num_types; @@ -599,9 +623,9 @@ private function persistType(object $type, object $typeCopy): void $this->put($typeCopy, 'ptr', $new); if ($first) { for ($i = 0; $i < $list->num_types; $i++) { - $entrySource = Core::pointerAtAddress('zend_type *', $listSource + $entryBase + $i * $typeSize); + $entrySource = Core::pointerAtAddress(zend_type::class, $listSource + $entryBase + $i * $typeSize); $entryCopy = $this->phase === 2 - ? Core::pointerAtAddress('zend_type *', $new + $entryBase + $i * $typeSize) + ? Core::pointerAtAddress(zend_type::class, $new + $entryBase + $i * $typeSize) : $entrySource; $this->persistType($entrySource, $entryCopy); } @@ -619,7 +643,7 @@ private function persistType(object $type, object $typeCopy): void /** Persists a pointed-to zend_function unit (function table entries, hooks, closures) */ private function persistFunction(int $source): int { - $opArray = Core::pointerAtAddress('zend_op_array *', $source); + $opArray = Core::pointerAtAddress(zend_op_array::class, $source); if ($opArray->type !== Core::engineConstant('ZEND_USER_FUNCTION')) { throw OpCacheException::unsupportedPayload('only user functions can be persisted into a file-cache image'); } @@ -634,8 +658,8 @@ private function persistFunction(int $source): int /** The shared field walk for pointed-to op_arrays and the embedded main_op_array */ private function persistOpArrayBody(int $source, int $copy): void { - $op = Core::pointerAtAddress('zend_op_array *', $source); - $opCopy = $this->phase === 2 ? Core::pointerAtAddress('zend_op_array *', $copy) : $op; + $op = Core::pointerAtAddress(zend_op_array::class, $source); + $opCopy = $this->phase === 2 ? Core::pointerAtAddress(zend_op_array::class, $copy) : $op; $staticVariables = $this->ptrValue($op, 'static_variables'); if ($staticVariables !== 0) { @@ -675,9 +699,9 @@ private function persistOpArrayBody(int $source, int $copy): void $this->put($opCopy, 'arg_info', $new + $hasRet * $argSize); if ($first) { for ($i = 0; $i < $entries; $i++) { - $entrySource = Core::pointerAtAddress('zend_arg_info *', $allocStart + $i * $argSize); + $entrySource = Core::pointerAtAddress(zend_arg_info::class, $allocStart + $i * $argSize); $entryCopy = $this->phase === 2 - ? Core::pointerAtAddress('zend_arg_info *', $new + $i * $argSize) + ? Core::pointerAtAddress(zend_arg_info::class, $new + $i * $argSize) : $entrySource; $nameAddress = $this->ptrValue($entrySource, 'name'); if ($nameAddress !== 0) { @@ -758,8 +782,8 @@ private function persistClassEntry(int $source): int if (!$first) { return $ceNew; } - $ce = Core::pointerAtAddress('zend_class_entry *', $source); - $ceCopy = $this->phase === 2 ? Core::pointerAtAddress('zend_class_entry *', $ceNew) : $ce; + $ce = Core::pointerAtAddress(zend_class_entry::class, $source); + $ceCopy = $this->phase === 2 ? Core::pointerAtAddress(zend_class_entry::class, $ceNew) : $ce; $this->put($ceCopy, 'name', $this->persistString($this->ptrValue($ce, 'name'))); if ($this->ptrValue($ce, 'parent') !== 0) { @@ -771,10 +795,10 @@ private function persistClassEntry(int $source): int } $this->persistHashData($ce->function_table, $ceCopy->function_table, function (int $zvalSource, int $zvalCopy): void { - $zv = Core::pointerAtAddress('zval *', $zvalSource); + $zv = Core::pointerAtAddress(zval::class, $zvalSource); $new = $this->persistFunction($this->ptrValue($zv->value, 'func')); if ($this->phase === 2) { - $this->put(Core::pointerAtAddress('zval *', $zvalCopy)->value, 'func', $new); + $this->put(Core::pointerAtAddress(zval::class, $zvalCopy)->value, 'func', $new); } }); @@ -797,17 +821,17 @@ private function persistClassEntry(int $source): int } $this->persistHashData($ce->constants_table, $ceCopy->constants_table, function (int $zvalSource, int $zvalCopy): void { - $zv = Core::pointerAtAddress('zval *', $zvalSource); + $zv = Core::pointerAtAddress(zval::class, $zvalSource); $constSource = $this->ptrValue($zv->value, 'ptr'); [$new, $first] = $this->unit($constSource, Core::sizeOfType(zend_class_constant::class)); if ($this->phase === 2) { - $this->put(Core::pointerAtAddress('zval *', $zvalCopy)->value, 'ptr', $new); + $this->put(Core::pointerAtAddress(zval::class, $zvalCopy)->value, 'ptr', $new); } if (!$first) { return; } - $constant = Core::pointerAtAddress('zend_class_constant *', $constSource); - $constantCopy = $this->phase === 2 ? Core::pointerAtAddress('zend_class_constant *', $new) : $constant; + $constant = Core::pointerAtAddress(zend_class_constant::class, $constSource); + $constantCopy = $this->phase === 2 ? Core::pointerAtAddress(zend_class_constant::class, $new) : $constant; $this->persistZval($constSource, $this->phase === 2 ? $new : $constSource); // value zval is the first member $docComment = $this->ptrValue($constant, 'doc_comment'); if ($docComment !== 0) { @@ -829,17 +853,17 @@ private function persistClassEntry(int $source): int $this->persistAttributes($ce, $ceCopy, 'attributes'); $this->persistHashData($ce->properties_info, $ceCopy->properties_info, function (int $zvalSource, int $zvalCopy): void { - $zv = Core::pointerAtAddress('zval *', $zvalSource); + $zv = Core::pointerAtAddress(zval::class, $zvalSource); $propSource = $this->ptrValue($zv->value, 'ptr'); [$new, $first] = $this->unit($propSource, Core::sizeOfType(zend_property_info::class)); if ($this->phase === 2) { - $this->put(Core::pointerAtAddress('zval *', $zvalCopy)->value, 'ptr', $new); + $this->put(Core::pointerAtAddress(zval::class, $zvalCopy)->value, 'ptr', $new); } if (!$first) { return; } - $prop = Core::pointerAtAddress('zend_property_info *', $propSource); - $propCopy = $this->phase === 2 ? Core::pointerAtAddress('zend_property_info *', $new) : $prop; + $prop = Core::pointerAtAddress(zend_property_info::class, $propSource); + $propCopy = $this->phase === 2 ? Core::pointerAtAddress(zend_property_info::class, $new) : $prop; $this->defer($propCopy, 'ce', $this->ptrValue($prop, 'ce'), 'property scope'); $this->put($propCopy, 'name', $this->persistString($this->ptrValue($prop, 'name'))); $docComment = $this->ptrValue($prop, 'doc_comment'); @@ -906,8 +930,8 @@ private function persistClassEntry(int $source): int [$new, $first] = $this->unit($iteratorFuncs, Core::sizeOfType(zend_class_iterator_funcs::class)); $this->put($ceCopy, 'iterator_funcs_ptr', $new); if ($first) { - $funcs = Core::pointerAtAddress('zend_class_iterator_funcs *', $iteratorFuncs); - $funcsCopy = $this->phase === 2 ? Core::pointerAtAddress('zend_class_iterator_funcs *', $new) : $funcs; + $funcs = Core::pointerAtAddress(zend_class_iterator_funcs::class, $iteratorFuncs); + $funcsCopy = $this->phase === 2 ? Core::pointerAtAddress(zend_class_iterator_funcs::class, $new) : $funcs; foreach (self::ITERATOR_FUNC_FIELDS as $field) { $target = $this->ptrValue($funcs, $field); if ($target !== 0) { @@ -921,8 +945,8 @@ private function persistClassEntry(int $source): int [$new, $first] = $this->unit($arrayAccessFuncs, Core::sizeOfType(zend_class_arrayaccess_funcs::class)); $this->put($ceCopy, 'arrayaccess_funcs_ptr', $new); if ($first) { - $funcs = Core::pointerAtAddress('zend_class_arrayaccess_funcs *', $arrayAccessFuncs); - $funcsCopy = $this->phase === 2 ? Core::pointerAtAddress('zend_class_arrayaccess_funcs *', $new) : $funcs; + $funcs = Core::pointerAtAddress(zend_class_arrayaccess_funcs::class, $arrayAccessFuncs); + $funcsCopy = $this->phase === 2 ? Core::pointerAtAddress(zend_class_arrayaccess_funcs::class, $new) : $funcs; foreach (self::ARRAYACCESS_FUNC_FIELDS as $field) { $target = $this->ptrValue($funcs, $field); if ($target !== 0) { @@ -940,10 +964,6 @@ private function persistClassEntry(int $source): int return $ceNew; } - /** - * @param \FFI\CData $ce - * @param \FFI\CData $ceCopy - */ private function persistClassNames(object $ce, object $ceCopy, string $field, int $count): void { $source = $this->ptrValue($ce, $field); @@ -957,19 +977,15 @@ private function persistClassNames(object $ce, object $ceCopy, string $field, in return; } for ($i = 0; $i < $count; $i++) { - $entrySource = Core::pointerAtAddress('zend_class_name *', $source + $i * $nameSize); + $entrySource = Core::pointerAtAddress(zend_class_name::class, $source + $i * $nameSize); $entryCopy = $this->phase === 2 - ? Core::pointerAtAddress('zend_class_name *', $new + $i * $nameSize) + ? Core::pointerAtAddress(zend_class_name::class, $new + $i * $nameSize) : $entrySource; $this->put($entryCopy, 'name', $this->persistString($this->ptrValue($entrySource, 'name'))); $this->put($entryCopy, 'lc_name', $this->persistString($this->ptrValue($entrySource, 'lc_name'))); } } - /** - * @param \FFI\CData $ce - * @param \FFI\CData $ceCopy - */ private function persistTraitAliases(object $ce, object $ceCopy): void { $source = $this->ptrValue($ce, 'trait_aliases'); @@ -992,8 +1008,8 @@ private function persistTraitAliases(object $ce, object $ceCopy): void if (!$firstAlias) { continue; } - $alias = Core::pointerAtAddress('zend_trait_alias *', $aliasSource); - $aliasCopy = $this->phase === 2 ? Core::pointerAtAddress('zend_trait_alias *', $newAlias) : $alias; + $alias = Core::pointerAtAddress(zend_trait_alias::class, $aliasSource); + $aliasCopy = $this->phase === 2 ? Core::pointerAtAddress(zend_trait_alias::class, $newAlias) : $alias; foreach (['method_name', 'class_name'] as $nameField) { $address = $this->ptrValue($alias->trait_method, $nameField); if ($address !== 0) { @@ -1007,10 +1023,6 @@ private function persistTraitAliases(object $ce, object $ceCopy): void } } - /** - * @param \FFI\CData $ce - * @param \FFI\CData $ceCopy - */ private function persistTraitPrecedences(object $ce, object $ceCopy): void { $source = $this->ptrValue($ce, 'trait_precedences'); @@ -1028,7 +1040,7 @@ private function persistTraitPrecedences(object $ce, object $ceCopy): void } for ($i = 0; $i < $count; $i++) { $precedenceSource = $this->slotValue($source + $i * PHP_INT_SIZE); - $precedence = Core::pointerAtAddress('zend_trait_precedence *', $precedenceSource); + $precedence = Core::pointerAtAddress(zend_trait_precedence::class, $precedenceSource); $size = Core::sizeOfType(zend_trait_precedence::class) + PHP_INT_SIZE * ($precedence->num_excludes - 1); [$newPrecedence, $firstPrecedence] = $this->unit($precedenceSource, $size); @@ -1037,7 +1049,7 @@ private function persistTraitPrecedences(object $ce, object $ceCopy): void continue; } $precedenceCopy = $this->phase === 2 - ? Core::pointerAtAddress('zend_trait_precedence *', $newPrecedence) + ? Core::pointerAtAddress(zend_trait_precedence::class, $newPrecedence) : $precedence; foreach (['method_name', 'class_name'] as $nameField) { $address = $this->ptrValue($precedence->trait_method, $nameField); @@ -1061,23 +1073,23 @@ private function persistTraitPrecedences(object $ce, object $ceCopy): void private function persistScript(int $source): void { [$new, ] = $this->unit($source, Core::sizeOfType(zend_persistent_script::class)); - $script = Core::pointerAtAddress('zend_persistent_script *', $source); - $scriptCopy = $this->phase === 2 ? Core::pointerAtAddress('zend_persistent_script *', $new) : $script; + $script = Core::pointerAtAddress(zend_persistent_script::class, $source); + $scriptCopy = $this->phase === 2 ? Core::pointerAtAddress(zend_persistent_script::class, $new) : $script; $this->put($scriptCopy->script, 'filename', $this->persistString($this->ptrValue($script->script, 'filename'))); $this->persistHashData($script->script->class_table, $scriptCopy->script->class_table, function (int $zvalSource, int $zvalCopy): void { - $zv = Core::pointerAtAddress('zval *', $zvalSource); + $zv = Core::pointerAtAddress(zval::class, $zvalSource); $new = $this->persistClassEntry($this->ptrValue($zv->value, 'ce')); if ($this->phase === 2) { - $this->put(Core::pointerAtAddress('zval *', $zvalCopy)->value, 'ce', $new); + $this->put(Core::pointerAtAddress(zval::class, $zvalCopy)->value, 'ce', $new); } }); $this->persistHashData($script->script->function_table, $scriptCopy->script->function_table, function (int $zvalSource, int $zvalCopy): void { - $zv = Core::pointerAtAddress('zval *', $zvalSource); + $zv = Core::pointerAtAddress(zval::class, $zvalSource); $new = $this->persistFunction($this->ptrValue($zv->value, 'func')); if ($this->phase === 2) { - $this->put(Core::pointerAtAddress('zval *', $zvalCopy)->value, 'func', $new); + $this->put(Core::pointerAtAddress(zval::class, $zvalCopy)->value, 'func', $new); } }); @@ -1097,8 +1109,8 @@ private function persistScript(int $source): void if (!$firstWarning) { continue; } - $warning = Core::pointerAtAddress('zend_error_info *', $warningSource); - $warningCopy = $this->phase === 2 ? Core::pointerAtAddress('zend_error_info *', $newWarning) : $warning; + $warning = Core::pointerAtAddress(zend_error_info::class, $warningSource); + $warningCopy = $this->phase === 2 ? Core::pointerAtAddress(zend_error_info::class, $newWarning) : $warning; foreach (['filename', 'message'] as $stringField) { $address = $this->ptrValue($warning, $stringField); if ($address !== 0) { @@ -1116,9 +1128,9 @@ private function persistScript(int $source): void $this->put($scriptCopy, 'early_bindings', $new); if ($first) { for ($i = 0; $i < $script->num_early_bindings; $i++) { - $bindingSource = Core::pointerAtAddress('zend_early_binding *', $earlyBindings + $i * $bindingSize); + $bindingSource = Core::pointerAtAddress(zend_early_binding::class, $earlyBindings + $i * $bindingSize); $bindingCopy = $this->phase === 2 - ? Core::pointerAtAddress('zend_early_binding *', $new + $i * $bindingSize) + ? Core::pointerAtAddress(zend_early_binding::class, $new + $i * $bindingSize) : $bindingSource; foreach (['lcname', 'rtd_key', 'lc_parent_name'] as $stringField) { $address = $this->ptrValue($bindingSource, $stringField); diff --git a/tests/OpCache/BoundsValidationTest.php b/tests/OpCache/BoundsValidationTest.php index c966405..f63f435 100644 --- a/tests/OpCache/BoundsValidationTest.php +++ b/tests/OpCache/BoundsValidationTest.php @@ -17,6 +17,7 @@ use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use ZEngine\Core; +use ZEngine\Generated\zend_script; /** * Bounds-validation coverage (issue #123): a crafted or truncated binary whose @@ -99,7 +100,11 @@ public function testHostileScriptPointerFieldIsRefused(): void // Corrupt the script's filename offset to a wild value past the region. // zend_script is the first member of zend_persistent_script, so the // script offset is a zend_script* - single-hop to keep the field typed. - $script = Core::pointerAtAddress('zend_script *', $base + $meta->scriptOffset()); + $script = Core::pointerAtAddress(zend_script::class, $base + $meta->scriptOffset()); + // The compiled fixture always carries a filename block + \assert($script->filename !== null); + // FFI::addr must stay inline on the pointer-field access to yield the filename SLOT address + // @phpstan-ignore argument.type (FFI::addr of a zend_string* pointer field) $filenameAt = Core::addressOf(FFI::addr($script->filename)); Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $filenameAt))[0] = $meta->memSize() + 0x4000; @@ -115,8 +120,8 @@ public function testHostileHashCountIsRefused(): void $base = Core::addressOf(Core::addr($buffer)); // Blow up the function table's nNumUsed so the bucket walk would spill - $script = Core::pointerAtAddress('zend_script *', $base + $meta->scriptOffset()); - $functionTableAt = Core::addressOf(FFI::addr($script->function_table)); + $script = Core::pointerAtAddress(zend_script::class, $base + $meta->scriptOffset()); + $functionTableAt = Core::addressOf(Core::addr($script->function_table)); $functionTable = Core::pointerAtAddress('HashTable *', $functionTableAt); $functionTable->nNumUsed = 0x7fffffff; @@ -133,7 +138,11 @@ public function testHostileInternedStringOffsetIsRefused(): void // Tag the script filename as an interned reference far past the (empty) // string section - a plausible-looking but out-of-range interned offset - $script = Core::pointerAtAddress('zend_script *', $base + $meta->scriptOffset()); + $script = Core::pointerAtAddress(zend_script::class, $base + $meta->scriptOffset()); + // The compiled fixture always carries a filename block + \assert($script->filename !== null); + // FFI::addr must stay inline on the pointer-field access to yield the filename SLOT address + // @phpstan-ignore argument.type (FFI::addr of a zend_string* pointer field) $filenameAt = Core::addressOf(FFI::addr($script->filename)); Core::cast('uintptr_t *', Core::pointerAtAddress('void *', $filenameAt))[0] = 0x100001; // odd => tagged, offset 0x100000