diff --git a/src/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollector.php b/src/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollector.php index a8244b5f..2e411427 100644 --- a/src/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollector.php +++ b/src/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollector.php @@ -334,6 +334,7 @@ public function collectAll( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); $call_frames_context = $this->collectCallFrames( @@ -343,6 +344,7 @@ public function collectAll( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); $defined_functions_context = $this->collectFunctionTable( @@ -362,6 +364,7 @@ public function collectAll( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); $global_constants_context = $this->collectGlobalConstants( @@ -371,6 +374,7 @@ public function collectAll( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); $objects_store_context = $this->collectObjectsStore( @@ -380,6 +384,7 @@ public function collectAll( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); if ($memory_limit_error_details and !is_null($this->memory_limit_error_function_context)) { @@ -426,7 +431,8 @@ public function collectZval( Dereferencer $dereferencer, ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, - ContextPools $context_pools + ContextPools $context_pools, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): ?ReferenceContext { if ($zval->isArray()) { assert(!is_null($zval->value->arr)); @@ -437,6 +443,7 @@ public function collectZval( $dereferencer, $zend_type_reader, $context_pools, + $memory_limit_error_details, ); } elseif ($zval->isObject()) { assert(!is_null($zval->value->obj)); @@ -447,6 +454,7 @@ public function collectZval( $dereferencer, $zend_type_reader, $context_pools, + $memory_limit_error_details, ); } elseif ($zval->isString()) { assert(!is_null($zval->value->str)); @@ -478,6 +486,7 @@ public function collectZval( $dereferencer, $zend_type_reader, $context_pools, + $memory_limit_error_details, ); } elseif ($zval->isResource()) { assert(!is_null($zval->value->res)); @@ -498,6 +507,7 @@ public function collectZval( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); } return null; @@ -536,7 +546,8 @@ public function collectPhpReferencePointer( MemoryLocations $memory_locations, Dereferencer $dereferencer, ZendTypeReader $zend_type_reader, - ContextPools $context_pools + ContextPools $context_pools, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): PhpReferenceContext { if ($memory_locations->has($pointer->address)) { $memory_location = $memory_locations->get($pointer->address); @@ -561,6 +572,7 @@ public function collectPhpReferencePointer( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); if (!is_null($zval_context)) { $php_referencecontext->add('referenced', $zval_context); @@ -575,7 +587,8 @@ public function collectZendArrayPointer( MemoryLocations $memory_locations, Dereferencer $dereferencer, ZendTypeReader $zend_type_reader, - ContextPools $context_pools + ContextPools $context_pools, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): ArrayHeaderContext { if ($memory_locations->has($pointer->address)) { $memory_location = $memory_locations->get($pointer->address); @@ -594,6 +607,7 @@ public function collectZendArrayPointer( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); } @@ -604,7 +618,8 @@ public function collectZendObjectPointer( MemoryLocations $memory_locations, Dereferencer $dereferencer, ZendTypeReader $zend_type_reader, - ContextPools $context_pools + ContextPools $context_pools, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): ObjectContext { if ($memory_locations->has($pointer->address)) { $memory_location = $memory_locations->get($pointer->address); @@ -626,6 +641,7 @@ public function collectZendObjectPointer( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); } @@ -666,6 +682,7 @@ public function collectCallFrames( ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, ContextPools $context_pools, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): CallFramesContext { $call_frames_context = new CallFramesContext(); if (is_null($eg->current_execute_data)) { @@ -680,6 +697,7 @@ public function collectCallFrames( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); $call_frames_context->add((string)$key, $call_frame_context); } @@ -757,6 +775,7 @@ public function collectRealCallStackOnMemoryLimitViolation( $zend_type_reader, $memory_locations, $context_pools, + null, ); $call_frames_context->add((string)($frame_no + $frame_start), $call_frame_context); } @@ -783,6 +802,7 @@ public function collectCallFrame( ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, ContextPools $context_pools, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): CallFrameContext { $function_name = $execute_data->getFullyQualifiedFunctionName( $dereferencer, @@ -817,6 +837,7 @@ public function collectCallFrame( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); if (!is_null($this_context)) { $call_frame_context->add('this', $this_context); @@ -834,6 +855,7 @@ public function collectCallFrame( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); if (!is_null($local_variable_context)) { $variable_table_context->add($name, $local_variable_context); @@ -852,6 +874,7 @@ public function collectCallFrame( $dereferencer, $zend_type_reader, $context_pools, + $memory_limit_error_details, ); $call_frame_context->add('symbol_table', $symbol_table_context); } @@ -863,6 +886,7 @@ public function collectCallFrame( $dereferencer, $zend_type_reader, $context_pools, + $memory_limit_error_details, ); $call_frame_context->add('extra_named_params', $extra_named_params_context); } @@ -875,7 +899,8 @@ public function collectGlobalVariables( Dereferencer $dereferencer, ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, - ContextPools $context_pools + ContextPools $context_pools, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): GlobalVariablesContext { return GlobalVariablesContext::fromArrayContext( $this->collectZendArray( @@ -885,6 +910,7 @@ public function collectGlobalVariables( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ) ); } @@ -895,7 +921,8 @@ public function collectZendArray( Dereferencer $dereferencer, ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, - ContextPools $context_pools + ContextPools $context_pools, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): ArrayHeaderContext { $array_header_location = ZendArrayMemoryLocation::fromZendArray($array); $array_table_location = ZendArrayTableMemoryLocation::fromZendArray($array); @@ -938,6 +965,7 @@ public function collectZendArray( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); if (!is_null($value_context)) { $element_context->add('value', $value_context); @@ -954,7 +982,8 @@ public function collectZendObject( Dereferencer $dereferencer, ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, - ContextPools $context_pools + ContextPools $context_pools, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): ObjectContext { $object_location = ZendObjectMemoryLocation::fromZendObject( $object, @@ -996,6 +1025,7 @@ public function collectZendObject( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); if (!is_null($property_context)) { $object_properties_context->add($name, $property_context); @@ -1018,6 +1048,7 @@ public function collectZendObject( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); $object_context->add('dynamic_properties', $dynamic_properties_context); } @@ -1037,6 +1068,7 @@ public function collectZendObject( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); $object_context->add('closure', $closure_context); } @@ -1051,6 +1083,7 @@ public function collectClosure( ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, ContextPools $context_pools, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): ClosureContext { $closure_context = new ClosureContext(); $closure_context->add( @@ -1062,6 +1095,7 @@ public function collectClosure( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ) ); $zval_context = $this->collectZval( @@ -1071,6 +1105,7 @@ public function collectClosure( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); if (!is_null($zval_context)) { $closure_context->add( @@ -1088,7 +1123,7 @@ public function collectFunctionTable( ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, ContextPools $context_pools, - MemoryLimitErrorDetails $memory_limit_error_details = null, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): DefinedFunctionsContext { $array_header_location = ZendArrayMemoryLocation::fromZendArray($array); $array_table_location = ZendArrayTableMemoryLocation::fromZendArray($array); @@ -1131,7 +1166,7 @@ public function collectZendFunctionPointer( ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, ContextPools $context_pools, - MemoryLimitErrorDetails $memory_limit_error_details = null, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): FunctionDefinitionContext { if ($memory_locations->has($pointer->address)) { $memory_location = $memory_locations->get($pointer->address); @@ -1161,7 +1196,7 @@ public function collectZendFunction( ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, ContextPools $context_pools, - MemoryLimitErrorDetails $memory_limit_error_details = null, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): FunctionDefinitionContext { if ($func->isUserFunction()) { $function_definition_context = $this->collectUserFunctionDefinition( @@ -1195,7 +1230,7 @@ public function collectUserFunctionDefinition( ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, ContextPools $context_pools, - MemoryLimitErrorDetails $memory_limit_error_details = null, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): UserFunctionDefinitionContext { $function_name = $func->getFullyQualifiedFunctionName( $dereferencer, @@ -1292,6 +1327,7 @@ public function collectUserFunctionDefinition( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); $op_array_context->add('static_variables', $static_variables_context); } @@ -1372,7 +1408,8 @@ public function collectClassConstantsTable( Dereferencer $dereferencer, ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, - ContextPools $context_pools + ContextPools $context_pools, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): ClassConstantsContext { $array_table_location = ZendArrayTableMemoryLocation::fromZendArray($array); $array_table_overhead_location = ZendArrayTableOverheadMemoryLocation::fromZendArrayAndUsedLocation( @@ -1423,6 +1460,7 @@ public function collectClassConstantsTable( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); if (!is_null($value_context)) { $constant_context->add('value', $value_context); @@ -1439,7 +1477,7 @@ public function collectClassTable( ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, ContextPools $context_pools, - ?MemoryLimitErrorDetails $memory_limit_error_details = null, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): DefinedClassesContext { $defined_classes_context = new DefinedClassesContext(); foreach ($array->getItemIterator($dereferencer) as $class_name => $zval) { @@ -1468,7 +1506,7 @@ private function collectClassDefinitionPointer( ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, ContextPools $context_pools, - ?MemoryLimitErrorDetails $memory_limit_error_details = null, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): ?ClassDefinitionContext { if ($memory_locations->has($pointer->address)) { return null; @@ -1492,7 +1530,7 @@ private function collectClassDefinition( ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, ContextPools $context_pools, - ?MemoryLimitErrorDetails $memory_limit_error_details = null, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): ClassDefinitionContext { $class_definition_context = new ClassDefinitionContext($class_entry->isInternal()); $memory_location = ZendClassEntryMemoryLocation::fromZendClassEntry($class_entry); @@ -1557,6 +1595,7 @@ private function collectClassDefinition( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); if (!is_null($static_property_context)) { $static_properties_context->add($name, $static_property_context); @@ -1614,6 +1653,7 @@ private function collectClassDefinition( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); $class_definition_context->add('constants', $class_constants_context); @@ -1667,7 +1707,8 @@ private function collectGlobalConstants( Dereferencer $dereferencer, ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, - ContextPools $context_pools + ContextPools $context_pools, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): GlobalConstantsContext { $memory_location = ZendArrayTableMemoryLocation::fromZendArray($array); $memory_locations->add($memory_location); @@ -1704,6 +1745,7 @@ private function collectGlobalConstants( $zend_type_reader, $memory_locations, $context_pools, + $memory_limit_error_details, ); if (!is_null($value_context)) { $constant_context->add('value', $value_context); @@ -1753,6 +1795,7 @@ private function collectInternedStrings( $zend_type_reader, $memory_locations, $context_pools, + null ); } @@ -1762,7 +1805,8 @@ private function collectObjectsStore( Dereferencer $dereferencer, ZendTypeReader $zend_type_reader, MemoryLocations $memory_locations, - ContextPools $context_pools + ContextPools $context_pools, + ?MemoryLimitErrorDetails $memory_limit_error_details, ): ObjectsStoreContext { $objects_store_memory_location = ObjectsStoreMemoryLocation::fromZendObjectsStore( $objects_store, @@ -1797,6 +1841,7 @@ private function collectObjectsStore( $dereferencer, $zend_type_reader, $context_pools, + $memory_limit_error_details, ); $objects_store_context->add((string)$key, $objects_store_bucket_context); } diff --git a/tests/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollectorTest.php b/tests/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollectorTest.php index d5f6d8d4..d1db14c9 100644 --- a/tests/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollectorTest.php +++ b/tests/Lib/PhpProcessReader/PhpMemoryReader/MemoryLocationsCollectorTest.php @@ -438,4 +438,291 @@ function f() { ->lineno ); } + + public function testMemoryLimitViolationOnMethod() + { + $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(); + } + } + (new C)->f(); + CODE + ], + [ + ['pipe', 'r'], + ['pipe', 'w'], + ['pipe', 'w'] + ], + $pipes + ); + + $child_status = proc_get_status($this->child); + $error_json = fgets($pipes[1]); + $php_symbol_reader_creator = new PhpSymbolReaderCreator( + $memory_reader, + new ProcessModuleSymbolReaderCreator( + new Elf64SymbolResolverCreator( + new CatFileReader(), + new Elf64Parser( + new LittleEndianReader() + ) + ), + $memory_reader, + new PerBinarySymbolCacheRetriever(), + ), + ProcessMemoryMapCreator::create(), + new LittleEndianReader() + ); + $php_globals_finder = new PhpGlobalsFinder( + $php_symbol_reader_creator, + new LittleEndianReader(), + new MemoryReader() + ); + + /** @var int $child_status['pid'] */ + $executor_globals_address = $php_globals_finder->findExecutorGlobals( + new ProcessSpecifier($child_status['pid']), + new TargetPhpSettings() + ); + $compiler_globals_address = $php_globals_finder->findCompilerGlobals( + new ProcessSpecifier($child_status['pid']), + new TargetPhpSettings() + ); + + $memory_locations_collector = new MemoryLocationsCollector( + $memory_reader, + $type_reader_creator, + new PhpZendMemoryManagerChunkFinder( + ProcessMemoryMapCreator::create(), + $type_reader_creator, + $php_globals_finder + ) + ); + $error = json_decode($error_json, true); + $collected_memories = $memory_locations_collector->collectAll( + new ProcessSpecifier($child_status['pid']), + new TargetPhpSettings(php_version: ZendTypeReader::V81), + $executor_globals_address, + $compiler_globals_address, + new MemoryLimitErrorDetails( + $error['file'], + $error['line'], + 512 + ) + ); + $this->assertGreaterThan(0, $collected_memories->memory_get_usage_size); + $this->assertGreaterThan(0, $collected_memories->memory_get_usage_real_size); + $this->assertGreaterThan( + 2, + $collected_memories->top_reference_context->call_frames->getFrameCount() + ); + $this->assertSame( + 'C::f', + $collected_memories->top_reference_context->call_frames + ->getFrameAt(3) + ->function_name + ); + $this->assertSame( + 16, + $collected_memories->top_reference_context->call_frames + ->getFrameAt(3) + ->lineno + ); + $this->assertSame( + 0x1000, + $collected_memories->top_reference_context->call_frames + ->getFrameAt(3) + ->getLocalVariable('var') + ->getElements() + ->getCount() + ); + $this->assertSame( + 0x1000, + $collected_memories->top_reference_context->call_frames + ->getFrameAt(4) + ->getLocalVariable('var') + ->getElements() + ->getCount() + ); + $last_frame = $collected_memories->top_reference_context->call_frames->getFrameCount() - 1; + $this->assertSame( + '
', + $collected_memories->top_reference_context->call_frames + ->getFrameAt($last_frame) + ->function_name + ); + $this->assertSame( + 19, + $collected_memories->top_reference_context->call_frames + ->getFrameAt($last_frame) + ->lineno + ); + } + + public function testMemoryLimitViolationOnClosure() + { + $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(); + }; + $f(); + } + } + (new C)->f(); + CODE + ], + [ + ['pipe', 'r'], + ['pipe', 'w'], + ['pipe', 'w'] + ], + $pipes + ); + + $child_status = proc_get_status($this->child); + $error_json = fgets($pipes[1]); + $php_symbol_reader_creator = new PhpSymbolReaderCreator( + $memory_reader, + new ProcessModuleSymbolReaderCreator( + new Elf64SymbolResolverCreator( + new CatFileReader(), + new Elf64Parser( + new LittleEndianReader() + ) + ), + $memory_reader, + new PerBinarySymbolCacheRetriever(), + ), + ProcessMemoryMapCreator::create(), + new LittleEndianReader() + ); + $php_globals_finder = new PhpGlobalsFinder( + $php_symbol_reader_creator, + new LittleEndianReader(), + new MemoryReader() + ); + + /** @var int $child_status['pid'] */ + $executor_globals_address = $php_globals_finder->findExecutorGlobals( + new ProcessSpecifier($child_status['pid']), + new TargetPhpSettings() + ); + $compiler_globals_address = $php_globals_finder->findCompilerGlobals( + new ProcessSpecifier($child_status['pid']), + new TargetPhpSettings() + ); + + $memory_locations_collector = new MemoryLocationsCollector( + $memory_reader, + $type_reader_creator, + new PhpZendMemoryManagerChunkFinder( + ProcessMemoryMapCreator::create(), + $type_reader_creator, + $php_globals_finder + ) + ); + $error = json_decode($error_json, true); + $collected_memories = $memory_locations_collector->collectAll( + new ProcessSpecifier($child_status['pid']), + new TargetPhpSettings(php_version: ZendTypeReader::V81), + $executor_globals_address, + $compiler_globals_address, + new MemoryLimitErrorDetails( + $error['file'], + $error['line'], + 512 + ) + ); + $this->assertGreaterThan(0, $collected_memories->memory_get_usage_size); + $this->assertGreaterThan(0, $collected_memories->memory_get_usage_real_size); + $this->assertGreaterThan( + 2, + $collected_memories->top_reference_context->call_frames->getFrameCount() + ); + $this->assertSame( + 'C::{closure}(Command line code:15-18)', + $collected_memories->top_reference_context->call_frames + ->getFrameAt(3) + ->function_name + ); + $this->assertSame( + 17, + $collected_memories->top_reference_context->call_frames + ->getFrameAt(3) + ->lineno + ); + $this->assertSame( + 0x1000, + $collected_memories->top_reference_context->call_frames + ->getFrameAt(3) + ->getLocalVariable('var') + ->getElements() + ->getCount() + ); + $this->assertSame( + 0x1000, + $collected_memories->top_reference_context->call_frames + ->getFrameAt(4) + ->getLocalVariable('var') + ->getElements() + ->getCount() + ); + $last_frame = $collected_memories->top_reference_context->call_frames->getFrameCount() - 1; + $this->assertSame( + '
', + $collected_memories->top_reference_context->call_frames + ->getFrameAt($last_frame) + ->function_name + ); + $this->assertSame( + 22, + $collected_memories->top_reference_context->call_frames + ->getFrameAt($last_frame) + ->lineno + ); + } }