diff --git a/lib/private/Log/ExceptionSerializer.php b/lib/private/Log/ExceptionSerializer.php index e5029a41d165a..272ff8f868ca2 100644 --- a/lib/private/Log/ExceptionSerializer.php +++ b/lib/private/Log/ExceptionSerializer.php @@ -170,7 +170,12 @@ private function encodeArg(mixed $arg, int $nestingLevel = 5): mixed { } $objectInfo = [ '__class__' => get_class($arg) ]; - $objectVars = get_object_vars($arg); + try { + $objectVars = get_object_vars($arg); + } catch (\Throwable $e) { + // get_object_vars may throw if $arg was a ghost and failed to build + $objectVars = ['__failure__' => $e->getMessage()]; + } return array_map(function ($arg) use ($nestingLevel) { return $this->encodeArg($arg, $nestingLevel - 1); }, array_merge($objectInfo, $objectVars)); diff --git a/tests/lib/Log/ExceptionSerializerTest.php b/tests/lib/Log/ExceptionSerializerTest.php index fd2406ff2ebb1..ec3c1699bf985 100644 --- a/tests/lib/Log/ExceptionSerializerTest.php +++ b/tests/lib/Log/ExceptionSerializerTest.php @@ -11,19 +11,37 @@ use OC\Log\ExceptionSerializer; use OC\SystemConfig; +use PHPUnit\Framework\Attributes\RequiresPhp; use Test\TestCase; +class LazyObjectFixture { + public string $value = 'value'; +} + class ExceptionSerializerTest extends TestCase { private ExceptionSerializer $serializer; + private string|false $exceptionIgnoreArgs; #[\Override] public function setUp(): void { parent::setUp(); + // traces only contain arguments if this is disabled + $this->exceptionIgnoreArgs = ini_set('zend.exception_ignore_args', '0'); + $config = $this->createMock(SystemConfig::class); $this->serializer = new ExceptionSerializer($config); } + #[\Override] + public function tearDown(): void { + if ($this->exceptionIgnoreArgs !== false) { + ini_set('zend.exception_ignore_args', $this->exceptionIgnoreArgs); + } + + parent::tearDown(); + } + private function emit($arguments) { \call_user_func_array([$this, 'bind'], $arguments); } @@ -48,6 +66,10 @@ private function throwsException(string $maybeSecret): void { throw new \Exception('expected exception'); } + private function throwsWithObject(object $object): void { + throw new \Exception('expected exception'); + } + /** * this test ensures that the serializer does not overwrite referenced * variables. It is crafted after a scenario we experienced: the DAV server @@ -92,4 +114,20 @@ public function testSerializerWithSensitiveParameter(): void { $this->assertSame(ExceptionSerializer::SENSITIVE_VALUE_PLACEHOLDER, $serializedData['Trace'][1]['args'][1]); } } + + #[RequiresPhp('>= 8.4')] + public function testSerializerDoesNotInitializeLazyObjects(): void { + $ghost = (new \ReflectionClass(LazyObjectFixture::class))->newLazyGhost(function (): void { + throw new \RuntimeException('Lazy ghost could not be initialized'); + }); + + try { + $this->throwsWithObject($ghost); + } catch (\Exception $e) { + $serializedData = $this->serializer->serializeException($e); + } + + $this->assertSame(LazyObjectFixture::class, $serializedData['Trace'][0]['args'][0]['__class__']); + $this->assertSame('Lazy ghost could not be initialized', $serializedData['Trace'][0]['args'][0]['__failure__']); + } }