From ef65b044ff44ae3f6eb9da9475f494e7fa0e2e02 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sat, 6 Jun 2026 19:04:35 +0000 Subject: [PATCH] Fix DECLARE_TRAIT MCJIT segfault via VM deferral gate (#6284) Empty trait compilation units segfaulted bin/jit.php -l because MCJIT link ran before DECLARE_TRAIT lowering was stable. Defer all script-scope TYPE_DECLARE_TRAIT to VM lowering (extends the trait-ctor gate) until native MCJIT is fixed in lib/JIT.php. Co-authored-by: Cursor --- lib/Block.php | 32 +++++++ test/repro/trait_empty_mcjit_probe.php | 3 + test/unit/TraitEmptyMcjitCompileTest.php | 106 +++++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 test/repro/trait_empty_mcjit_probe.php create mode 100644 test/unit/TraitEmptyMcjitCompileTest.php diff --git a/lib/Block.php b/lib/Block.php index ed769ac74..89ae99688 100755 --- a/lib/Block.php +++ b/lib/Block.php @@ -1988,6 +1988,37 @@ private static function propertyFetchDestUsedAsAssignLvalue(self $block, int $op return false; } + /** + * Any DECLARE_TRAIT in the compilation unit — MCJIT link/execute segfaults (#3609, #6284). + */ + public static function containsDeclareTraitOpcodesInScriptScope(?self $root): bool + { + if (null === $root) { + return false; + } + $seen = new \SplObjectStorage(); + $stack = [$root]; + while ([] !== $stack) { + $block = array_pop($stack); + if (!$block instanceof self || $seen->contains($block)) { + continue; + } + $seen->attach($block); + foreach ($block->opCodes as $op) { + if (OpCode::TYPE_DECLARE_TRAIT === $op->type) { + return true; + } + foreach ([$op->block1, $op->block2, $op->block3] as $sub) { + if ($sub instanceof self) { + $stack[] = $sub; + } + } + } + } + + return false; + } + /** * Trait `__construct` merged into a using class — MCJIT execute segfaults (#4671). */ @@ -2175,6 +2206,7 @@ public static function requiresVmLowering(?self $root): bool || self::containsUserClassDeclaredInstancePropertyOpcodes($root) || self::containsDynamicPropertyDeprecationOpcodes($root) || self::containsFiberSuspendOpcodesInScriptScope($root) + || self::containsDeclareTraitOpcodesInScriptScope($root) || self::containsTraitConstructorOpcodes($root) || self::containsReflectionAttributeNewInstanceOpcodes($root) || self::containsInterfaceAbstractStaticMcjitDeferral($root) diff --git a/test/repro/trait_empty_mcjit_probe.php b/test/repro/trait_empty_mcjit_probe.php new file mode 100644 index 000000000..3b642614f --- /dev/null +++ b/test/repro/trait_empty_mcjit_probe.php @@ -0,0 +1,3 @@ +repoRoot = dirname(__DIR__, 2); + LlvmToolchain::applyCurrentProcessEnv($this->repoRoot); + if (!LlvmToolchain::isReady($this->repoRoot)) { + $reason = LlvmToolchain::readyFailureReason() ?? 'LLVM 9 toolchain not available'; + $this->markTestSkipped($reason.' — trait MCJIT compile test needs LLVM (#6284)'); + } + } + + public function testEmptyTraitDefersMcjitUntilStable(): void + { + $runtime = new Runtime(); + $block = $runtime->parseAndCompile(<<<'PHP' +assertNotNull($block); + $this->assertTrue(Block::containsDeclareTraitOpcodesInScriptScope($block)); + $this->assertTrue(Block::requiresVmLowering($block)); + } + + public function testEmptyTraitMcjitCompileOnlyViaBinJit(): void + { + $script = $this->repoRoot.'/test/repro/trait_empty_mcjit_probe.php'; + $this->assertFileExists($script); + $jit = realpath($this->repoRoot.'/bin/jit.php'); + $this->assertNotFalse($jit); + $env = $_ENV; + $env['PHP_COMPILER_SKIP_LLVM_PRELOAD'] = '1'; + $proc = proc_open( + array_merge(LlvmToolchain::envPrefix($this->repoRoot), [PHP_BINARY, $jit, '-l', $script]), + [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes, + $this->repoRoot, + $env + ); + $this->assertIsResource($proc); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $exit = proc_close($proc); + $this->assertSame(0, $exit, 'MCJIT compile-only must not segfault: '.$stderr); + } + + public function testEmptyTraitExecutesViaVmFallback(): void + { + $varDir = $this->repoRoot.'/var'; + if (!is_dir($varDir) && !mkdir($varDir, 0775, true) && !is_dir($varDir)) { + $this->fail('Could not create var/ for trait MCJIT execute script'); + } + $script = $varDir.'/trait_empty_mcjit_run_'.getmypid().'.php'; + file_put_contents($script, <<<'PHP' +repoRoot.'/bin/jit.php'); + $this->assertNotFalse($jit); + $env = $_ENV; + $env['PHP_COMPILER_SKIP_LLVM_PRELOAD'] = '1'; + $proc = proc_open( + array_merge(LlvmToolchain::envPrefix($this->repoRoot), [PHP_BINARY, $jit, $script]), + [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes, + $this->repoRoot, + $env + ); + $this->assertIsResource($proc); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $exit = proc_close($proc); + $this->assertSame(0, $exit, trim($stderr)); + $this->assertSame("ok\n", $stdout); + } finally { + @unlink($script); + } + } +}