diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 3137d544..9c028d3e 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -49,7 +49,10 @@ jobs: - name: Run test suite run: | mkdir -p build/logs - ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml + composer test-for-ci + + - name: Fix absolute paths in the coverage report + run: sed -i "s| /dev/null \ + && apt-get update -y \ + && apt-get install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=composer /usr/bin/composer /usr/bin/composer + +WORKDIR /app +COPY . . + +RUN composer install + +CMD ["php", "vendor/bin/phpunit", "--colors=always", "--testdox", "--coverage-text", "--coverage-clover=coverage.xml", "--coverage-html=coverage"] diff --git a/composer.json b/composer.json index e9eb42b3..8a95d631 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,8 @@ }, "autoload-dev": { "psr-4": { - "Reli\\": "tests" + "Reli\\": "tests", + "Reli\\Command\\": "tests/Command/CommandEnumeratorTestData" } }, "bin": [ @@ -56,7 +57,13 @@ ], "scripts": { "test": [ - "phpunit" + "docker-compose run reli-test" + ], + "test-with-coverage": [ + "docker-compose run reli-test-with-coverage" + ], + "test-for-ci": [ + "docker-compose run reli-test-for-ci" ], "psalm": [ "psalm.phar" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..624101ca --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,49 @@ +version: '3.9' +services: + reli-test: + build: + context: . + dockerfile: Dockerfile-dev + pid: "host" + cap_add: + - SYS_PTRACE + security_opt: + - seccomp:unconfined + volumes: + - .:/app + - /var/run/docker.sock:/var/run/docker.sock + - /tmp/reli-test:/tmp/reli-test + container_name: reli-test + command: ["vendor/bin/phpunit" , "--colors=always", "--testdox"] + reli-test-with-coverage: + build: + context: . + dockerfile: Dockerfile-dev + pid: "host" + cap_add: + - SYS_PTRACE + security_opt: + - seccomp:unconfined + volumes: + - .:/app + - /var/run/docker.sock:/var/run/docker.sock + - /tmp/reli-test:/tmp/reli-test + - ./build:/app/build + container_name: reli-test + command: ["vendor/bin/phpunit" , "--colors=always", "--testdox", "--coverage-clover", "build/logs/clover.xml"] + reli-test-for-ci: + build: + context: . + dockerfile: Dockerfile-dev + pid: "host" + cap_add: + - SYS_PTRACE + security_opt: + - seccomp:unconfined + volumes: + - .:/app + - /var/run/docker.sock:/var/run/docker.sock + - /tmp/reli-test:/tmp/reli-test + - ./build:/app/build + container_name: reli-test + command: ["vendor/bin/phpunit" , "--coverage-clover", "build/logs/clover.xml"] diff --git a/src/Lib/PhpInternals/Headers/v70.h b/src/Lib/PhpInternals/Headers/v70.h index b2c08305..9711c11a 100644 --- a/src/Lib/PhpInternals/Headers/v70.h +++ b/src/Lib/PhpInternals/Headers/v70.h @@ -167,6 +167,17 @@ struct _zend_resource { void *ptr; }; +// zend_compile.h +typedef struct _zend_property_info { + uint32_t offset; /* property offset for object properties or + property index for static properties */ + uint32_t flags; + zend_string *name; + zend_string *doc_comment; + zend_class_entry *ce; +} zend_property_info; + +// zend_types.h struct _zend_reference { zend_refcounted_h gc; zval val; diff --git a/src/Lib/PhpInternals/Headers/v71.h b/src/Lib/PhpInternals/Headers/v71.h index ec4b778d..029d3133 100644 --- a/src/Lib/PhpInternals/Headers/v71.h +++ b/src/Lib/PhpInternals/Headers/v71.h @@ -169,6 +169,17 @@ struct _zend_resource { void *ptr; }; +// zend_compile.h +typedef struct _zend_property_info { + uint32_t offset; /* property offset for object properties or + property index for static properties */ + uint32_t flags; + zend_string *name; + zend_string *doc_comment; + zend_class_entry *ce; +} zend_property_info; + +// zend_types.h struct _zend_reference { zend_refcounted_h gc; zval val; diff --git a/src/Lib/PhpInternals/Headers/v72.h b/src/Lib/PhpInternals/Headers/v72.h index 86179048..fc3d2f65 100644 --- a/src/Lib/PhpInternals/Headers/v72.h +++ b/src/Lib/PhpInternals/Headers/v72.h @@ -171,6 +171,17 @@ struct _zend_resource { typedef uintptr_t zend_type; +// zend_compile.h +typedef struct _zend_property_info { + uint32_t offset; /* property offset for object properties or + property index for static properties */ + uint32_t flags; + zend_string *name; + zend_string *doc_comment; + zend_class_entry *ce; +} zend_property_info; + +// zend_types.h struct _zend_reference { zend_refcounted_h gc; zval val; diff --git a/src/Lib/PhpInternals/Headers/v73.h b/src/Lib/PhpInternals/Headers/v73.h index 4995b486..ff033ca2 100644 --- a/src/Lib/PhpInternals/Headers/v73.h +++ b/src/Lib/PhpInternals/Headers/v73.h @@ -168,6 +168,17 @@ struct _zend_resource { typedef uintptr_t zend_type; +// zend_compile.h +typedef struct _zend_property_info { + uint32_t offset; /* property offset for object properties or + property index for static properties */ + uint32_t flags; + zend_string *name; + zend_string *doc_comment; + zend_class_entry *ce; +} zend_property_info; + +// zend_types.h struct _zend_reference { zend_refcounted_h gc; zval val; diff --git a/src/Lib/PhpInternals/Types/Zend/V74/ZendArray.php b/src/Lib/PhpInternals/Types/Zend/V74/ZendArray.php index 56c178d5..b0797a82 100644 --- a/src/Lib/PhpInternals/Types/Zend/V74/ZendArray.php +++ b/src/Lib/PhpInternals/Types/Zend/V74/ZendArray.php @@ -33,6 +33,37 @@ public function __get(string $field_name): mixed }; } + public function dumpFlags(): string + { + $flags = $this->flags; + $flag_names = []; + if ($flags & ((1 << 0) | (1 << 1))) { + $flag_names[] = 'HASH_FLAG_CONSISTENCY'; + } + if ($flags & (1 << 2)) { + $flag_names[] = 'HASH_FLAG_PACKED'; + } + if ($flags & (1 << 3)) { + $flag_names[] = 'HASH_FLAG_UNINITIALIZED'; + } + if ($flags & (1 << 4)) { + $flag_names[] = 'HASH_FLAG_STATIC_KEYS'; + } + if ($flags & (1 << 5)) { + $flag_names[] = 'HASH_FLAG_HAS_EMPTY_IND'; + } + if ($flags & (1 << 6)) { + $flag_names[] = 'HASH_FLAG_ALLOW_COW_VIOLATION'; + } + + return implode(' | ', $flag_names); + } + + public function isUninitialized(): bool + { + return (bool)($this->flags & (1 << 3)); + } + public function getDataSize(): int { return $this->nTableSize * self::BUCKET_SIZE_IN_BYTES; diff --git a/src/Lib/PhpInternals/Types/Zend/ZendArray.php b/src/Lib/PhpInternals/Types/Zend/ZendArray.php index 27023c70..c25fd627 100644 --- a/src/Lib/PhpInternals/Types/Zend/ZendArray.php +++ b/src/Lib/PhpInternals/Types/Zend/ZendArray.php @@ -351,6 +351,7 @@ public static function fromCastedCData(CastedCData $casted_cdata, Pointer $point return new static($casted_cdata, $pointer); } + /** @return Pointer */ public function getPointer(): Pointer { return $this->pointer; diff --git a/src/Lib/PhpInternals/Types/Zend/ZendClassEntry.php b/src/Lib/PhpInternals/Types/Zend/ZendClassEntry.php index 54e1b950..5ee1d21f 100644 --- a/src/Lib/PhpInternals/Types/Zend/ZendClassEntry.php +++ b/src/Lib/PhpInternals/Types/Zend/ZendClassEntry.php @@ -198,7 +198,8 @@ public function iteratePropertyInfo( Dereferencer $dereferencer, ZendTypeReader $type_reader, ): iterable { - foreach ($this->properties_info->getItemIterator($dereferencer) as $name => $item) { + $property_info = $dereferencer->deref($this->properties_info->getPointer()); + foreach ($property_info->getItemIterator($dereferencer) as $name => $item) { $property_info_pointer = $item->value->getAsPointer( ZendPropertyInfo::class, $type_reader->sizeOf(ZendPropertyInfo::getCTypeName()), diff --git a/src/Lib/PhpInternals/Types/Zend/ZendExecuteData.php b/src/Lib/PhpInternals/Types/Zend/ZendExecuteData.php index 6cdcb70e..226cfb3f 100644 --- a/src/Lib/PhpInternals/Types/Zend/ZendExecuteData.php +++ b/src/Lib/PhpInternals/Types/Zend/ZendExecuteData.php @@ -259,7 +259,9 @@ public function getRootFrame( public function getVariableTableAddress(): int { - return $this->pointer->indexedAt(1)->address; + return (int)($this->pointer->address + + (int)((($this->pointer->size) + 16 - 1) / 16) * 16 + ); } public function getTotalVariablesNum(Dereferencer $dereferencer): int diff --git a/src/Lib/PhpProcessReader/CallTraceReader/CallTraceReader.php b/src/Lib/PhpProcessReader/CallTraceReader/CallTraceReader.php index 303af4ef..f76e8fa6 100644 --- a/src/Lib/PhpProcessReader/CallTraceReader/CallTraceReader.php +++ b/src/Lib/PhpProcessReader/CallTraceReader/CallTraceReader.php @@ -15,17 +15,19 @@ use Reli\Lib\PhpInternals\Opcodes\OpcodeFactory; use Reli\Lib\PhpInternals\Types\C\RawDouble; +use Reli\Lib\PhpInternals\Types\Zend\Bucket; use Reli\Lib\PhpInternals\Types\Zend\Opline; +use Reli\Lib\PhpInternals\Types\Zend\ZendArray; use Reli\Lib\PhpInternals\Types\Zend\ZendCastedTypeProvider; -use Reli\Lib\PhpInternals\Types\Zend\ZendExecuteData; use Reli\Lib\PhpInternals\Types\Zend\ZendExecutorGlobals; -use Reli\Lib\PhpInternals\Types\Zend\ZendFunction; use Reli\Lib\PhpInternals\Types\Zend\ZendOp; +use Reli\Lib\PhpInternals\Types\Zend\Zval; use Reli\Lib\PhpInternals\ZendTypeReader; use Reli\Lib\PhpInternals\ZendTypeReaderCreator; use Reli\Lib\Process\MemoryReader\MemoryReaderInterface; use Reli\Lib\Process\MemoryReader\MemoryReaderException; use Reli\Lib\Process\Pointer\Dereferencer; +use Reli\Lib\Process\Pointer\PointedTypeResolver; use Reli\Lib\Process\Pointer\Pointer; use Reli\Lib\Process\Pointer\RemoteProcessDereferencer; use Reli\Lib\Process\ProcessSpecifier; @@ -62,7 +64,46 @@ private function getDereferencer(int $pid, string $php_version): Dereferencer new ProcessSpecifier($pid), new ZendCastedTypeProvider( $this->getTypeReader($php_version), - ) + ), + new class ($php_version) implements PointedTypeResolver { + public function __construct( + private string $php_version, + ) { + } + + public function resolve(string $type_name): string + { + return match ($this->php_version) { + ZendTypeReader::V70, + ZendTypeReader::V71, + ZendTypeReader::V72 => match ($type_name) { + Bucket::class => \Reli\Lib\PhpInternals\Types\Zend\V70\Bucket::class, + ZendArray::class => \Reli\Lib\PhpInternals\Types\Zend\V70\ZendArray::class, + Zval::class => \Reli\Lib\PhpInternals\Types\Zend\V70\Zval::class, + default => $type_name, + }, + ZendTypeReader::V73 => match ($type_name) { + Bucket::class => \Reli\Lib\PhpInternals\Types\Zend\V73\Bucket::class, + ZendArray::class => \Reli\Lib\PhpInternals\Types\Zend\V73\ZendArray::class, + Zval::class => \Reli\Lib\PhpInternals\Types\Zend\V73\Zval::class, + default => $type_name, + }, + ZendTypeReader::V74 => match ($type_name) { + Bucket::class => \Reli\Lib\PhpInternals\Types\Zend\V74\Bucket::class, + ZendArray::class => \Reli\Lib\PhpInternals\Types\Zend\V74\ZendArray::class, + Zval::class => \Reli\Lib\PhpInternals\Types\Zend\V74\Zval::class, + default => $type_name, + }, + ZendTypeReader::V80, + ZendTypeReader::V81 => match ($type_name) { + ZendArray::class => \Reli\Lib\PhpInternals\Types\Zend\V80\ZendArray::class, + default => $type_name, + }, + ZendTypeReader::V82, + ZendTypeReader::V83 => $type_name, + }; + } + } ); } diff --git a/src/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocation/ZendObjectMemoryLocation.php b/src/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocation/ZendObjectMemoryLocation.php index 70781138..4d4a361a 100644 --- a/src/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocation/ZendObjectMemoryLocation.php +++ b/src/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocation/ZendObjectMemoryLocation.php @@ -39,7 +39,10 @@ public static function fromZendObject( $class_name = $ce->getClassName($dereferencer); if ($class_name === \Fiber::class) { $size = $zend_type_reader->sizeOf('zend_fiber'); - } elseif ($class_name === \Closure::class) { + } elseif ( + $class_name === \Closure::class + and !$zend_type_reader->isPhpVersionLowerThan(ZendTypeReader::V71) + ) { $size = $zend_type_reader->sizeOf('zend_closure'); } else { $size = $zend_object->getMemorySize($dereferencer); diff --git a/src/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollector.php b/src/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollector.php index 2e411427..32b39b88 100644 --- a/src/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollector.php +++ b/src/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollector.php @@ -1055,7 +1055,10 @@ public function collectZendObject( assert(!is_null($object->ce)); $class_entry = $dereferencer->deref($object->ce); - if ($class_entry->getClassName($dereferencer) === 'Closure') { + if ( + $class_entry->getClassName($dereferencer) === 'Closure' + and !$zend_type_reader->isPhpVersionLowerThan(ZendTypeReader::V71) + ) { $closure_context = $this->collectClosure( $dereferencer->deref( ZendClosure::getPointerFromZendObjectPointer( @@ -1636,7 +1639,7 @@ private function collectClassDefinition( } $methods_context = $this->collectFunctionTable( - $class_entry->function_table, + $dereferencer->deref($class_entry->function_table->getPointer()), $map_ptr_base, $dereferencer, $zend_type_reader, @@ -1647,7 +1650,7 @@ private function collectClassDefinition( $class_definition_context->add('methods', $methods_context); $class_constants_context = $this->collectClassConstantsTable( - $class_entry->constants_table, + $dereferencer->deref($class_entry->constants_table->getPointer()), $map_ptr_base, $dereferencer, $zend_type_reader, diff --git a/tests/Command/CommandEnumeratorTestData/Test1Directory/Test1Command.php b/tests/Command/CommandEnumeratorTestData/Test1Directory/Test1Command.php index 5490ab7f..aec924b9 100644 --- a/tests/Command/CommandEnumeratorTestData/Test1Directory/Test1Command.php +++ b/tests/Command/CommandEnumeratorTestData/Test1Directory/Test1Command.php @@ -11,8 +11,10 @@ declare(strict_types=1); -namespace Reli\Command\CommandEnumeratorTestData\Test1Directory; +namespace Reli\Command\Test1Directory; -final class Test1Command +use Symfony\Component\Console\Command\Command; + +final class Test1Command extends Command { } diff --git a/tests/Command/CommandEnumeratorTestData/Test1Directory/Test2Command.php b/tests/Command/CommandEnumeratorTestData/Test1Directory/Test2Command.php index 866935a9..08879beb 100644 --- a/tests/Command/CommandEnumeratorTestData/Test1Directory/Test2Command.php +++ b/tests/Command/CommandEnumeratorTestData/Test1Directory/Test2Command.php @@ -11,8 +11,10 @@ declare(strict_types=1); -namespace Reli\Command\CommandEnumeratorTestData\Test1Directory; +namespace Reli\Command\Test1Directory; -final class Test2Command +use Symfony\Component\Console\Command\Command; + +final class Test2Command extends Command { } diff --git a/tests/Command/CommandEnumeratorTestData/Test2Directory/Test3Command.php b/tests/Command/CommandEnumeratorTestData/Test2Directory/Test3Command.php index 163ff002..5e4a3967 100644 --- a/tests/Command/CommandEnumeratorTestData/Test2Directory/Test3Command.php +++ b/tests/Command/CommandEnumeratorTestData/Test2Directory/Test3Command.php @@ -11,8 +11,10 @@ declare(strict_types=1); -namespace Reli\Command\CommandEnumeratorTestData\Test2Directory; +namespace Reli\Command\Test2Directory; -final class Test3Command +use Symfony\Component\Console\Command\Command; + +final class Test3Command extends Command { } diff --git a/tests/Command/CommandEnumeratorTestData/Test2Directory/Test4Command.php b/tests/Command/CommandEnumeratorTestData/Test2Directory/Test4Command.php index 250cee8c..450d6d68 100644 --- a/tests/Command/CommandEnumeratorTestData/Test2Directory/Test4Command.php +++ b/tests/Command/CommandEnumeratorTestData/Test2Directory/Test4Command.php @@ -11,8 +11,10 @@ declare(strict_types=1); -namespace Reli\Command\CommandEnumeratorTestData\Test2Directory; +namespace Reli\Command\Test2Directory; -final class Test4Command +use Symfony\Component\Console\Command\Command; + +final class Test4Command extends Command { } diff --git a/tests/Lib/PhpProcessReader/CallTraceReader/CallTraceReaderTest.php b/tests/Lib/PhpProcessReader/CallTraceReader/CallTraceReaderTest.php index d9182ce9..3b55853b 100644 --- a/tests/Lib/PhpProcessReader/CallTraceReader/CallTraceReaderTest.php +++ b/tests/Lib/PhpProcessReader/CallTraceReader/CallTraceReaderTest.php @@ -13,6 +13,7 @@ namespace Reli\Lib\PhpProcessReader\CallTraceReader; +use PHPUnit\Framework\Attributes\DataProviderExternal; use Reli\BaseTestCase; use Reli\Inspector\Settings\TargetPhpSettings\TargetPhpSettings; use Reli\Lib\ByteStream\IntegerByteSequence\LittleEndianReader; @@ -22,13 +23,13 @@ use Reli\Lib\Elf\SymbolResolver\Elf64SymbolResolverCreator; use Reli\Lib\File\CatFileReader; use Reli\Lib\PhpInternals\Opcodes\OpcodeFactory; -use Reli\Lib\PhpInternals\ZendTypeReader; use Reli\Lib\PhpInternals\ZendTypeReaderCreator; use Reli\Lib\PhpProcessReader\PhpGlobalsFinder; use Reli\Lib\PhpProcessReader\PhpSymbolReaderCreator; use Reli\Lib\Process\MemoryMap\ProcessMemoryMapCreator; use Reli\Lib\Process\MemoryReader\MemoryReader; use Reli\Lib\Process\ProcessSpecifier; +use Reli\TargetPhpVmProvider; class CallTraceReaderTest extends BaseTestCase { @@ -47,7 +48,8 @@ protected function tearDown(): void } } - public function testReadCallTrace() + #[DataProviderExternal(TargetPhpVmProvider::class, 'allSupported')] + public function testReadCallTrace(string $php_version, string $docker_image_name): void { $memory_reader = new MemoryReader(); $executor_globals_reader = new CallTraceReader( @@ -55,9 +57,7 @@ public function testReadCallTrace() new ZendTypeReaderCreator(), new OpcodeFactory() ); - $tmp_file = tempnam(sys_get_temp_dir(), 'reli-prof-test'); - file_put_contents( - $tmp_file, + $target_script = <<wait(); CODE - ); - $this->child = proc_open( - [ - PHP_BINARY, - $tmp_file, - ], - [ - ['pipe', 'r'], - ['pipe', 'w'], - ['pipe', 'w'] - ], + ; + $pipes = []; + [$this->child, $pid] = TargetPhpVmProvider::runScriptViaContainer( + $docker_image_name, + $target_script, $pipes ); - fgets($pipes[1]); + $s = fgets($pipes[1]); + $this->assertSame("a\n", $s); $child_status = proc_get_status($this->child); + $this->assertSame(true, $child_status['running']); $php_symbol_reader_creator = new PhpSymbolReaderCreator( $memory_reader, new ProcessModuleSymbolReaderCreator( @@ -108,17 +104,21 @@ public function wait() { /** @var int $child_status['pid'] */ $executor_globals_address = $php_globals_finder->findExecutorGlobals( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings() + new ProcessSpecifier($pid), + new TargetPhpSettings( + php_version: $php_version, + ) ); $sapi_globals_address = $php_globals_finder->findSAPIGlobals( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings() + new ProcessSpecifier($pid), + new TargetPhpSettings( + php_version: $php_version, + ) ); $call_trace = $executor_globals_reader->readCallTrace( - $child_status['pid'], - ZendTypeReader::V81, + $pid, + $php_version, $executor_globals_address, $sapi_globals_address, PHP_INT_MAX, @@ -142,7 +142,7 @@ public function wait() { $call_trace->call_frames[1]->getFullyQualifiedFunctionName() ); $this->assertSame( - $tmp_file, + '/source', $call_trace->call_frames[1]->file_name ); $this->assertSame( @@ -154,7 +154,7 @@ public function wait() { $call_trace->call_frames[2]->getFullyQualifiedFunctionName() ); $this->assertSame( - $tmp_file, + '/source', $call_trace->call_frames[2]->file_name ); $this->assertSame( diff --git a/tests/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollectorTest.php b/tests/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollectorTest.php index d1db14c9..545f14e8 100644 --- a/tests/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollectorTest.php +++ b/tests/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollectorTest.php @@ -13,6 +13,8 @@ namespace Reli\Lib\PhpProcessReader\PhpMemoryReader; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\DataProviderExternal; use Reli\BaseTestCase; use Reli\Inspector\Settings\MemoryProfilerSettings\MemoryLimitErrorDetails; use Reli\Inspector\Settings\TargetPhpSettings\TargetPhpSettings; @@ -34,6 +36,7 @@ use Reli\Lib\Process\MemoryMap\ProcessMemoryMapCreator; use Reli\Lib\Process\MemoryReader\MemoryReader; use Reli\Lib\Process\ProcessSpecifier; +use Reli\TargetPhpVmProvider; class MemoryLocationsCollectorTest extends BaseTestCase { @@ -62,50 +65,54 @@ protected function tearDown(): void } } - public function testCollectAll() + public static function provideFromV80() + { + yield from TargetPhpVmProvider::from(ZendTypeReader::V80); + } + + #[DataProvider('provideFromV80')] + public function testCollectAllFromV80(string $php_version, string $docker_image_name): void { $memory_reader = new MemoryReader(); $type_reader_creator = new ZendTypeReaderCreator(); - $this->child = proc_open( - [ - PHP_BINARY, - '-r', - <<<'CODE' - /** class doc_comment */ - class A { - public static $output = STDOUT; - - /** property doc_comment */ - public string $result = ''; - - /** function doc_comment */ - public function wait($input): void { - static $test_static_variable = 0xdeadbeef; - (function (...$_) use ($input) { - $this->result = fgets($input); - })(123, extra: 456); - } + $target_script = + <<<'CODE' + result = fgets($input); + })(123, extra: 456); } - $tempfile = tempnam('', ''); - include $tempfile; - $object = new A; - $ref_object =& $object; - $object->dynamic_property = 42; - fputs(A::$output, "a\n"); - $object->wait(STDIN); - CODE - ], - [ - ['pipe', 'r'], - ['pipe', 'w'], - ['pipe', 'w'] - ], + } + $tempfile = tempnam('', ''); + include $tempfile; + $object = new A; + $ref_object =& $object; + $object->dynamic_property = 42; + fputs(A::$output, "a\n"); + $object->wait(STDIN); + CODE + ; + $pipes = []; + [$this->child, $pid] = TargetPhpVmProvider::runScriptViaContainer( + $docker_image_name, + $target_script, $pipes ); + $s = fgets($pipes[1]); + $this->assertSame("a\n", $s); - fgets($pipes[1]); - $child_status = proc_get_status($this->child); $php_symbol_reader_creator = new PhpSymbolReaderCreator( $memory_reader, new ProcessModuleSymbolReaderCreator( @@ -127,14 +134,17 @@ public function wait($input): void { new MemoryReader() ); - /** @var int $child_status['pid'] */ $executor_globals_address = $php_globals_finder->findExecutorGlobals( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings() + new ProcessSpecifier($pid), + new TargetPhpSettings( + php_version: $php_version, + ) ); $compiler_globals_address = $php_globals_finder->findCompilerGlobals( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings() + new ProcessSpecifier($pid), + new TargetPhpSettings( + php_version: $php_version, + ) ); $memory_locations_collector = new MemoryLocationsCollector( @@ -147,8 +157,8 @@ public function wait($input): void { ) ); $collected_memories = $memory_locations_collector->collectAll( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings(php_version: ZendTypeReader::V81), + new ProcessSpecifier($pid), + new TargetPhpSettings(php_version: $php_version), $executor_globals_address, $compiler_globals_address ); @@ -245,7 +255,12 @@ public function wait($input): void { $contexts_analyzed['call_frames']['2']['function_name'] ); $this->assertSame( - $contexts_analyzed['call_frames']['3']['symbol_table']['array_elements']['object']['value']['#node_id'], + $contexts_analyzed + ['call_frames'] + ['3'] + ['local_variables'] + ['object'] + ['#node_id'], $contexts_analyzed ['call_frames'] ['3'] @@ -282,62 +297,63 @@ public function wait($input): void { $this->assertSame( 0xdeadbeef, $contexts_analyzed - ['class_table'] - ['a'] - ['methods'] - ['wait'] - ['op_array'] - ['static_variables'] - ['array_elements'] - ['test_static_variable'] - ['value'] - ['value'] + ['call_frames'] + ['2'] + ['local_variables'] + ['test_static_variable'] + ['referenced'] + ['value'] ); $this->assertSame( - 1, - $contexts_analyzed['included_files']['#count'] + 3, + $contexts_analyzed + ['included_files'] + ['#count'] ); } - public function testMemoryLimitViolation() + #[DataProviderExternal(TargetPhpVmProvider::class, 'allSupported')] + public function testMemoryLimitViolation(string $php_version, string $docker_image_name) { $memory_reader = new MemoryReader(); $type_reader_creator = new ZendTypeReaderCreator(); - $this->child = proc_open( - [ - PHP_BINARY, - '-r', - <<<'CODE' - ini_set('memory_limit', '2M'); - register_shutdown_function(function () { - $error = error_get_last(); - if (is_null($error)) { - return; - } - if (strpos($error['message'], 'Allowed memory size of') !== 0) { - return; - } - fputs(STDOUT, json_encode($error) . "\n"); - fgets(STDIN); - }); - function f() { - $var = array_fill(0, 0x1000, 0); - f(); + $target_script = + <<<'CODE' + child, $pid] = TargetPhpVmProvider::runScriptViaContainer( + $docker_image_name, + $target_script, $pipes ); - - $child_status = proc_get_status($this->child); + fgets($pipes[1]); + $error_message = fgets($pipes[1]); + $this->assertStringStartsWith( + 'Fatal error: Allowed memory size of', + $error_message + ); $error_json = fgets($pipes[1]); + $php_symbol_reader_creator = new PhpSymbolReaderCreator( $memory_reader, new ProcessModuleSymbolReaderCreator( @@ -359,14 +375,17 @@ function f() { new MemoryReader() ); - /** @var int $child_status['pid'] */ $executor_globals_address = $php_globals_finder->findExecutorGlobals( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings() + new ProcessSpecifier($pid), + new TargetPhpSettings( + php_version: $php_version, + ) ); $compiler_globals_address = $php_globals_finder->findCompilerGlobals( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings() + new ProcessSpecifier($pid), + new TargetPhpSettings( + php_version: $php_version, + ) ); $memory_locations_collector = new MemoryLocationsCollector( @@ -380,8 +399,8 @@ function f() { ); $error = json_decode($error_json, true); $collected_memories = $memory_locations_collector->collectAll( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings(php_version: ZendTypeReader::V81), + new ProcessSpecifier($pid), + new TargetPhpSettings(php_version: $php_version), $executor_globals_address, $compiler_globals_address, new MemoryLimitErrorDetails( @@ -393,7 +412,7 @@ function f() { $this->assertGreaterThan(0, $collected_memories->memory_get_usage_size); $this->assertGreaterThan(0, $collected_memories->memory_get_usage_real_size); $this->assertGreaterThan( - 2, + 3, $collected_memories->top_reference_context->call_frames->getFrameCount() ); $this->assertSame( @@ -403,7 +422,7 @@ function f() { ->function_name ); $this->assertSame( - 15, + $php_version >= ZendTypeReader::V81 ? 16 : 15, $collected_memories->top_reference_context->call_frames ->getFrameAt(3) ->lineno @@ -411,7 +430,7 @@ function f() { $this->assertSame( 0x1000, $collected_memories->top_reference_context->call_frames - ->getFrameAt(3) + ->getFrameAt(4) ->getLocalVariable('var') ->getElements() ->getCount() @@ -419,7 +438,7 @@ function f() { $this->assertSame( 0x1000, $collected_memories->top_reference_context->call_frames - ->getFrameAt(4) + ->getFrameAt(5) ->getLocalVariable('var') ->getElements() ->getCount() @@ -432,54 +451,57 @@ function f() { ->function_name ); $this->assertSame( - 17, + 18, $collected_memories->top_reference_context->call_frames ->getFrameAt($last_frame) ->lineno ); } - public function testMemoryLimitViolationOnMethod() + #[DataProviderExternal(TargetPhpVmProvider::class, 'allSupported')] + public function testMemoryLimitViolationOnMethod(string $php_version, string $docker_image_name) { $memory_reader = new MemoryReader(); $type_reader_creator = new ZendTypeReaderCreator(); - $this->child = proc_open( - [ - PHP_BINARY, - '-r', - <<<'CODE' - ini_set('memory_limit', '2M'); - register_shutdown_function(function () { - $error = error_get_last(); - if (is_null($error)) { - return; - } - if (strpos($error['message'], 'Allowed memory size of') !== 0) { - return; - } - fputs(STDOUT, json_encode($error) . "\n"); - fgets(STDIN); - }); - class C { - public function f() { - $var = array_fill(0, 0x1000, 0); - $this->f(); - } + $target_script = + <<<'CODE' + f(); - CODE - ], - [ - ['pipe', 'r'], - ['pipe', 'w'], - ['pipe', 'w'] - ], + if (strpos($error['message'], 'Allowed memory size of') !== 0) { + return; + } + fputs(STDOUT, json_encode($error) . "\n"); + fgets(STDIN); + }); + class C { + public function f() { + $var = array_fill(0, 0x1000, 0); + $this->f(); + } + } + (new C)->f(); + CODE + ; + $pipes = []; + [$this->child, $pid] = TargetPhpVmProvider::runScriptViaContainer( + $docker_image_name, + $target_script, $pipes ); - - $child_status = proc_get_status($this->child); + fgets($pipes[1]); + $error_message = fgets($pipes[1]); + $this->assertStringStartsWith( + 'Fatal error: Allowed memory size of', + $error_message + ); $error_json = fgets($pipes[1]); + $php_symbol_reader_creator = new PhpSymbolReaderCreator( $memory_reader, new ProcessModuleSymbolReaderCreator( @@ -501,14 +523,17 @@ public function f() { new MemoryReader() ); - /** @var int $child_status['pid'] */ $executor_globals_address = $php_globals_finder->findExecutorGlobals( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings() + new ProcessSpecifier($pid), + new TargetPhpSettings( + php_version: $php_version, + ) ); $compiler_globals_address = $php_globals_finder->findCompilerGlobals( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings() + new ProcessSpecifier($pid), + new TargetPhpSettings( + php_version: $php_version, + ) ); $memory_locations_collector = new MemoryLocationsCollector( @@ -522,8 +547,8 @@ public function f() { ); $error = json_decode($error_json, true); $collected_memories = $memory_locations_collector->collectAll( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings(php_version: ZendTypeReader::V81), + new ProcessSpecifier($pid), + new TargetPhpSettings(php_version: $php_version), $executor_globals_address, $compiler_globals_address, new MemoryLimitErrorDetails( @@ -535,7 +560,7 @@ public function f() { $this->assertGreaterThan(0, $collected_memories->memory_get_usage_size); $this->assertGreaterThan(0, $collected_memories->memory_get_usage_real_size); $this->assertGreaterThan( - 2, + 3, $collected_memories->top_reference_context->call_frames->getFrameCount() ); $this->assertSame( @@ -545,7 +570,7 @@ public function f() { ->function_name ); $this->assertSame( - 16, + $php_version >= ZendTypeReader::V81 ? 17 : 16, $collected_memories->top_reference_context->call_frames ->getFrameAt(3) ->lineno @@ -553,7 +578,7 @@ public function f() { $this->assertSame( 0x1000, $collected_memories->top_reference_context->call_frames - ->getFrameAt(3) + ->getFrameAt(4) ->getLocalVariable('var') ->getElements() ->getCount() @@ -561,7 +586,7 @@ public function f() { $this->assertSame( 0x1000, $collected_memories->top_reference_context->call_frames - ->getFrameAt(4) + ->getFrameAt(5) ->getLocalVariable('var') ->getElements() ->getCount() @@ -574,57 +599,71 @@ public function f() { ->function_name ); $this->assertSame( - 19, + 20, $collected_memories->top_reference_context->call_frames ->getFrameAt($last_frame) ->lineno ); } - public function testMemoryLimitViolationOnClosure() + + public static function provideFromV71() + { + yield from TargetPhpVmProvider::from(ZendTypeReader::V71); + } + + #[DataProvider('provideFromV71')] + public function testMemoryLimitViolationOnClosure(string $php_version, string $docker_image_name) { + if ($php_version === ZendTypeReader::V70) { + $this->markTestSkipped('V70 does not support closure frame'); + } + $memory_reader = new MemoryReader(); $type_reader_creator = new ZendTypeReaderCreator(); - $this->child = proc_open( - [ - PHP_BINARY, - '-r', - <<<'CODE' - ini_set('memory_limit', '2M'); - register_shutdown_function(function () { - $error = error_get_last(); - if (is_null($error)) { - return; - } - if (strpos($error['message'], 'Allowed memory size of') !== 0) { - return; - } - fputs(STDOUT, json_encode($error) . "\n"); - fgets(STDIN); - }); - class C { - public function f() { - $f = static function () use (&$f) { - $var = array_fill(0, 0x1000, 0); - $f(); - }; + $target_script = + <<<'CODE' + f(); - CODE - ], - [ - ['pipe', 'r'], - ['pipe', 'w'], - ['pipe', 'w'] - ], + } + (new C)->f(); + CODE + ; + + $pipes = []; + [$this->child, $pid] = TargetPhpVmProvider::runScriptViaContainer( + $docker_image_name, + $target_script, $pipes ); - - $child_status = proc_get_status($this->child); + fgets($pipes[1]); + $error_message = fgets($pipes[1]); + $this->assertStringStartsWith( + 'Fatal error: Allowed memory size of', + $error_message + ); $error_json = fgets($pipes[1]); + $php_symbol_reader_creator = new PhpSymbolReaderCreator( $memory_reader, new ProcessModuleSymbolReaderCreator( @@ -646,14 +685,17 @@ public function f() { new MemoryReader() ); - /** @var int $child_status['pid'] */ $executor_globals_address = $php_globals_finder->findExecutorGlobals( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings() + new ProcessSpecifier($pid), + new TargetPhpSettings( + php_version: $php_version, + ) ); $compiler_globals_address = $php_globals_finder->findCompilerGlobals( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings() + new ProcessSpecifier($pid), + new TargetPhpSettings( + php_version: $php_version, + ) ); $memory_locations_collector = new MemoryLocationsCollector( @@ -667,8 +709,8 @@ public function f() { ); $error = json_decode($error_json, true); $collected_memories = $memory_locations_collector->collectAll( - new ProcessSpecifier($child_status['pid']), - new TargetPhpSettings(php_version: ZendTypeReader::V81), + new ProcessSpecifier($pid), + new TargetPhpSettings(php_version: $php_version), $executor_globals_address, $compiler_globals_address, new MemoryLimitErrorDetails( @@ -684,13 +726,13 @@ public function f() { $collected_memories->top_reference_context->call_frames->getFrameCount() ); $this->assertSame( - 'C::{closure}(Command line code:15-18)', + 'C::{closure}(/source:16-19)', $collected_memories->top_reference_context->call_frames ->getFrameAt(3) ->function_name ); $this->assertSame( - 17, + $php_version >= ZendTypeReader::V81 ? 18 : 17, $collected_memories->top_reference_context->call_frames ->getFrameAt(3) ->lineno @@ -698,7 +740,7 @@ public function f() { $this->assertSame( 0x1000, $collected_memories->top_reference_context->call_frames - ->getFrameAt(3) + ->getFrameAt(4) ->getLocalVariable('var') ->getElements() ->getCount() @@ -706,7 +748,7 @@ public function f() { $this->assertSame( 0x1000, $collected_memories->top_reference_context->call_frames - ->getFrameAt(4) + ->getFrameAt(5) ->getLocalVariable('var') ->getElements() ->getCount() @@ -719,7 +761,7 @@ public function f() { ->function_name ); $this->assertSame( - 22, + 23, $collected_memories->top_reference_context->call_frames ->getFrameAt($last_frame) ->lineno diff --git a/tests/Lib/Process/MemoryMap/ProcessMemoryMapReaderTest.php b/tests/Lib/Process/MemoryMap/ProcessMemoryMapReaderTest.php index 572927b1..3495ccd0 100644 --- a/tests/Lib/Process/MemoryMap/ProcessMemoryMapReaderTest.php +++ b/tests/Lib/Process/MemoryMap/ProcessMemoryMapReaderTest.php @@ -22,7 +22,8 @@ public function testRead() $result = (new ProcessMemoryMapReader())->read(getmypid()); $first_line = strtok($result, "\n"); $this->assertMatchesRegularExpression( - '/[0-9a-f]+-[0-9a-f]+ [r\-][w\-][x\-][sp\-] [0-9a-f]+ [0-9][0-9][0-9]?:[0-9][0-9][0-9]? [0-9]+ +[^ ].*/', + // phpcs:ignore Generic.Files.LineLength.TooLong + '/[0-9a-f]+-[0-9a-f]+ [r\-][w\-][x\-][sp\-] [0-9a-f]+ [0-9a-z][0-9a-z][0-9a-z]?:[0-9a-z][0-9a-z][0-9a-z]? [0-9]+ +[^ ].*/', $first_line ); } diff --git a/tests/TargetPhpVmProvider.php b/tests/TargetPhpVmProvider.php new file mode 100644 index 00000000..253ab9fa --- /dev/null +++ b/tests/TargetPhpVmProvider.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Reli; + +use Reli\Lib\PhpInternals\ZendTypeReader; + +class TargetPhpVmProvider +{ + public static function from(string $php_version) + { + $versions = [ + ZendTypeReader::V70, + ZendTypeReader::V71, + ZendTypeReader::V72, + ZendTypeReader::V73, + ZendTypeReader::V74, + ZendTypeReader::V80, + ZendTypeReader::V81, + ZendTypeReader::V82, + ZendTypeReader::V83, + ]; + foreach ($versions as $v) { + if ($php_version <= $v) { + yield $v => [$v, self::dockerImageNameFromPhpVersion($v)]; + } + } + } + + public static function allSupported() + { + $versions = [ + ZendTypeReader::V70, + ZendTypeReader::V71, + ZendTypeReader::V72, + ZendTypeReader::V73, + ZendTypeReader::V74, + ZendTypeReader::V80, + ZendTypeReader::V81, + ZendTypeReader::V82, + ZendTypeReader::V83, + ]; + foreach ($versions as $version) { + yield $version => [$version, self::dockerImageNameFromPhpVersion($version)]; + } + } + + public static function dockerImageNameFromPhpVersion(string $php_version): string + { + return match ($php_version) { + ZendTypeReader::V70 => 'php:7.0-cli', + ZendTypeReader::V71 => 'php:7.1-cli', + ZendTypeReader::V72 => 'php:7.2-cli', + ZendTypeReader::V73 => 'php:7.3-cli', + ZendTypeReader::V74 => 'php:7.4-cli', + ZendTypeReader::V80 => 'php:8.0-cli', + ZendTypeReader::V81 => 'php:8.1-cli', + ZendTypeReader::V82 => 'php:8.2-cli', + ZendTypeReader::V83 => 'php:8.3-cli', + default => throw new \InvalidArgumentException("unsupported php version: $php_version"), + }; + } + + public static function runScriptViaContainer( + string $docker_image_name, + string $script, + array &$pipes, + ) { + $tmp_file = tempnam('/tmp/reli-test', 'reli-prof-test'); + $pid_writer = tempnam('/tmp/reli-test', 'reli-prof-test-pid-writer'); + $pid_file = tempnam('/tmp/reli-test', 'reli-prof-test-pid'); + + chmod($tmp_file, 0777); + chmod($pid_writer, 0777); + chmod($pid_file, 0777); + + file_put_contents( + $pid_writer, + << '/source', + $pid_writer => '/pid-writer', + $pid_file => '/target-pid', + '/tmp/reli-test' => '/tmp/reli-test', + ], + ); + $pid_written_message = fgets($pipes[1]); + assert($pid_written_message === "pid written\n"); + $pid = (int)file_get_contents($pid_file); + return [$proc_handle, $pid]; + } + + public static function procOpenViaDocker( + string $docker_image_name, + string $command, + array $descriptorspec, + array &$pipes, + array $mount_points = [], + ) { + $mount_options = array_map( + fn ($source, $target) => "-v$source:$target:rw", + array_keys($mount_points), + array_values($mount_points) + ); + $uid = posix_getuid(); + $gid = posix_getgid(); + + $docker_command = [ + 'docker', + 'run', + '--rm', + '-u', + "$uid:$gid", + '--pid', + 'host', + '-i', + '--entrypoint', + 'sh', + ...$mount_options, + $docker_image_name, + '-c', + $command, + ]; + return proc_open( + $docker_command, + $descriptorspec, + $pipes + ); + } +}