From 620f29b0cf830df9ebe37dfa1b73fa2e2b641595 Mon Sep 17 00:00:00 2001 From: PurHur Date: Tue, 7 Jul 2026 14:32:26 +0000 Subject: [PATCH] Fix forward-profile builtin leak on 8.2 reference profile (#17206) Gate mb_trim, crc32c, hebrevc, attribute_exists, and class_uses_recursive on CompilerVersion forward-profile predicates so unset PHP_COMPILER_PROFILE matches Zend 8.2 function_exists parity; forward profile 8.4 keeps callability. Co-authored-by: Cursor --- ext/standard/BuiltinIntrospectionPolicy.php | 3 + ext/standard/Module.php | 2 +- lib/CompilerVersion.php | 134 ++++++++++++++---- test/compliance/JITTest.php | 9 ++ test/compliance/VMTest.php | 9 ++ test/compliance/cases/stdlib/hebrevc.phpt | 2 + test/compliance/cases/stdlib/hebrevc_jit.phpt | 2 + .../cases/stdlib/hebrevc_phantom.phpt | 9 ++ .../forward_profile_builtin_leak_82.phpt | 11 ++ test/repro/maintainer_gap_crc32c_builtin.php | 4 +- ...er_gap_forward_profile_builtin_leak_82.php | 29 ++++ test/repro/maintainer_gap_hebrevc_missing.php | 2 + ...ompilerVersionBuiltinAdvertisementTest.php | 38 ++--- test/unit/CompilerVersionGateTest.php | 14 +- 14 files changed, 214 insertions(+), 54 deletions(-) create mode 100644 test/compliance/cases/stdlib/hebrevc_phantom.phpt create mode 100644 test/compliance/cases/stdlib/profile_gate/forward_profile_builtin_leak_82.phpt create mode 100644 test/repro/maintainer_gap_forward_profile_builtin_leak_82.php diff --git a/ext/standard/BuiltinIntrospectionPolicy.php b/ext/standard/BuiltinIntrospectionPolicy.php index 3aa4e4c35f3..1e5296663c8 100644 --- a/ext/standard/BuiltinIntrospectionPolicy.php +++ b/ext/standard/BuiltinIntrospectionPolicy.php @@ -29,6 +29,9 @@ public static function functionIsAdvertised(string $functionName): bool if ('crc32c' === $lc) { return CompilerVersion::advertisesCrc32c(); } + if ('hebrevc' === $lc) { + return CompilerVersion::advertisesHebrevc(); + } if (\in_array($lc, [ 'array_any', 'array_all', diff --git a/ext/standard/Module.php b/ext/standard/Module.php index 85ed05d68be..a04734b6192 100755 --- a/ext/standard/Module.php +++ b/ext/standard/Module.php @@ -250,7 +250,7 @@ public function getFunctions(): array new soundex(), new metaphone(), new hebrev(), - new hebrevc(), + ...(CompilerVersion::supportsHebrevc() ? [new hebrevc()] : []), ...(CompilerVersion::supportsConvertCyrString() ? [new convert_cyr_string()] : []), new strnatcmp(), new strnatcasecmp(), diff --git a/lib/CompilerVersion.php b/lib/CompilerVersion.php index 0ac97443041..47749b4a868 100644 --- a/lib/CompilerVersion.php +++ b/lib/CompilerVersion.php @@ -367,8 +367,8 @@ public static function advertisesStrIncrement(): bool /** * PHP 8.3+ class_uses_recursive() (ext/standard/basic_functions.c, issue #6469, #12816, #16708). * - * Enabled by default on the 8.4.0-dev toolchain (runtime reports 8.4; php-src has the builtin since 8.3). - * `PHP_COMPILER_PROFILE=8.2` (or older) keeps the Zend 8.2 reference gate (function absent). + * Withheld on 8.4.0-dev reference profile (matches Zend 8.2 function_exists gate). Enable via + * stable 8.4.0+ or explicit `PHP_COMPILER_PROFILE=8.3` / `8.4` forward profile. */ public static function supportsClassUsesRecursive(): bool { @@ -376,12 +376,16 @@ public static function supportsClassUsesRecursive(): bool return false; } + if (version_compare(self::VERSION, '8.4.0', '>=')) { + return true; + } + $raw = getenv('PHP_COMPILER_PROFILE'); - if (\is_string($raw) && '' !== trim($raw)) { - return version_compare(self::languageProfileVersion(), '8.3.0', '>='); + if (!\is_string($raw) || '' === trim($raw)) { + return false; } - return self::advertisesBuiltinSince('8.3.0'); + return version_compare(self::languageProfileVersion(), '8.3.0', '>='); } /** @@ -961,8 +965,8 @@ public static function supportsClassHasFunctions(): bool * PHP 8.4+ attribute_exists(), class_meth_exists(), unitenum_exists() * (ext/reflection/php_reflection.c, ext/standard/basic_functions.c; #14995, #15692, #17138). * - * Default 8.4.0-dev toolchain registers the builtins; withheld when - * {@see languageProfileVersion()} is below 8.4.0 (e.g. `PHP_COMPILER_PROFILE=8.2`). + * Withheld on 8.4.0-dev reference profile (matches Zend 8.2 function_exists gate). Enable via + * stable 8.4.0+ or explicit `PHP_COMPILER_PROFILE=8.4` forward profile. */ public static function supportsPhp84ReflectionProbeBuiltins(): bool { @@ -970,18 +974,31 @@ public static function supportsPhp84ReflectionProbeBuiltins(): bool return false; } + if (version_compare(self::VERSION, '8.4.0', '>=')) { + return true; + } + $raw = getenv('PHP_COMPILER_PROFILE'); - if (\is_string($raw) && '' !== trim($raw)) { - return version_compare(self::languageProfileVersion(), '8.4.0', '>='); + if (!\is_string($raw) || '' === trim($raw)) { + return false; } - return version_compare(self::builtinAdvertisementVersion(), '8.4.0', '>='); + return version_compare(self::languageProfileVersion(), '8.4.0', '>='); } - /** attribute_exists()/class_meth_exists()/unitenum_exists() visible to function_exists() — same gate as {@see supportsPhp84ReflectionProbeBuiltins()}. */ + /** attribute_exists()/class_meth_exists()/unitenum_exists() visible to function_exists() — stable runtime or forward 8.4+ (#17206). */ public static function advertisesPhp84ReflectionProbeBuiltins(): bool { - return self::supportsPhp84ReflectionProbeBuiltins(); + if (version_compare(self::VERSION, '8.4.0', '>=')) { + return true; + } + + $raw = getenv('PHP_COMPILER_PROFILE'); + if (!\is_string($raw) || '' === trim($raw)) { + return false; + } + + return version_compare(self::languageProfileVersion(), '8.4.0', '>='); } /** @@ -1593,8 +1610,8 @@ public static function supportsArrayPadPadType(): bool /** * PHP 8.4+ mb_trim/ltrim/rtrim (ext/mbstring/mbstring.c, issue #11901, #12797, #9977, #17120). * - * Default 8.4.0-dev toolchain registers the builtins; withheld when - * {@see languageProfileVersion()} is below 8.4.0 (e.g. `PHP_COMPILER_PROFILE=8.2`). + * Withheld on 8.4.0-dev reference profile (matches Zend 8.2 function_exists gate). Enable via + * stable 8.4.0+ or explicit `PHP_COMPILER_PROFILE=8.4` forward profile. */ public static function supportsMbTrimFunctions(): bool { @@ -1602,20 +1619,33 @@ public static function supportsMbTrimFunctions(): bool return false; } + if (version_compare(self::VERSION, '8.4.0', '>=')) { + return true; + } + $raw = getenv('PHP_COMPILER_PROFILE'); - if (\is_string($raw) && '' !== trim($raw)) { - return version_compare(self::languageProfileVersion(), '8.4.0', '>='); + if (!\is_string($raw) || '' === trim($raw)) { + return false; } - return version_compare(self::builtinAdvertisementVersion(), '8.4.0', '>='); + return version_compare(self::languageProfileVersion(), '8.4.0', '>='); } /** - * mb_trim/ltrim/rtrim visible to function_exists() — same gate as {@see supportsMbTrimFunctions()}. + * mb_trim/ltrim/rtrim visible to function_exists() — stable runtime or forward 8.4+ (#17206). */ public static function advertisesMbTrimFunctions(): bool { - return self::supportsMbTrimFunctions(); + if (version_compare(self::VERSION, '8.4.0', '>=')) { + return true; + } + + $raw = getenv('PHP_COMPILER_PROFILE'); + if (!\is_string($raw) || '' === trim($raw)) { + return false; + } + + return version_compare(self::languageProfileVersion(), '8.4.0', '>='); } /** @@ -1707,8 +1737,8 @@ public static function supportsDisktotalspace(): bool /** * PHP 8.3+ crc32c() (ext/standard/crc32.c, issue #3270, #17139). * - * Enabled by default on the 8.4.0-dev toolchain (runtime reports 8.4; php-src has the builtin since 8.3). - * `PHP_COMPILER_PROFILE=8.2` keeps the Zend 8.2 reference gate (function absent). + * Withheld on 8.4.0-dev reference profile (matches Zend 8.2 function_exists gate). Enable via + * stable 8.4.0+ or explicit `PHP_COMPILER_PROFILE=8.3` / `8.4` forward profile. */ public static function supportsCrc32c(): bool { @@ -1716,20 +1746,72 @@ public static function supportsCrc32c(): bool return false; } + if (version_compare(self::VERSION, '8.4.0', '>=')) { + return true; + } + $raw = getenv('PHP_COMPILER_PROFILE'); - if (\is_string($raw) && '' !== trim($raw)) { - return version_compare(self::languageProfileVersion(), '8.3.0', '>='); + if (!\is_string($raw) || '' === trim($raw)) { + return false; } - return self::advertisesBuiltinSince('8.3.0'); + return version_compare(self::languageProfileVersion(), '8.3.0', '>='); } /** - * crc32c() visible to function_exists() — same gate as {@see supportsCrc32c()} (#17139). + * crc32c() visible to function_exists() — stable runtime or forward 8.3+ (#17206). */ public static function advertisesCrc32c(): bool { - return self::supportsCrc32c(); + if (version_compare(self::VERSION, '8.4.0', '>=')) { + return true; + } + + $raw = getenv('PHP_COMPILER_PROFILE'); + if (!\is_string($raw) || '' === trim($raw)) { + return false; + } + + return version_compare(self::languageProfileVersion(), '8.3.0', '>='); + } + + /** + * PHP 8.3+ hebrevc() (ext/standard/string.c, issue #17183, #17206). + * + * Withheld on 8.4.0-dev reference profile (matches Zend 8.2 function_exists gate). Enable via + * stable 8.4.0+ or explicit `PHP_COMPILER_PROFILE=8.3` / `8.4` forward profile. + */ + public static function supportsHebrevc(): bool + { + if (version_compare(self::VERSION, '8.3', '<')) { + return false; + } + + if (version_compare(self::VERSION, '8.4.0', '>=')) { + return true; + } + + $raw = getenv('PHP_COMPILER_PROFILE'); + if (!\is_string($raw) || '' === trim($raw)) { + return false; + } + + return version_compare(self::languageProfileVersion(), '8.3.0', '>='); + } + + /** hebrevc() visible to function_exists() — stable runtime or forward 8.3+ (#17206). */ + public static function advertisesHebrevc(): bool + { + if (version_compare(self::VERSION, '8.4.0', '>=')) { + return true; + } + + $raw = getenv('PHP_COMPILER_PROFILE'); + if (!\is_string($raw) || '' === trim($raw)) { + return false; + } + + return version_compare(self::languageProfileVersion(), '8.3.0', '>='); } /** diff --git a/test/compliance/JITTest.php b/test/compliance/JITTest.php index fa15212a866..2e93f5a9ec8 100755 --- a/test/compliance/JITTest.php +++ b/test/compliance/JITTest.php @@ -151,6 +151,15 @@ public static function providePHPTests(): \Generator && !str_contains($name, 'crc32c_phantom')) { continue; } + if (!CompilerVersion::supportsHebrevc() + && str_contains($name, 'hebrevc') + && !str_contains($name, 'hebrevc_phantom')) { + continue; + } + if (CompilerVersion::supportsHebrevc() + && str_contains($name, 'hebrevc_phantom')) { + continue; + } if (!CompilerVersion::supportsMbStrPad() && str_contains($name, 'mb_str_pad') && !str_contains($name, 'mb_str_pad_phantom')) { diff --git a/test/compliance/VMTest.php b/test/compliance/VMTest.php index 9d7d4f7cdd7..584f00e6f12 100755 --- a/test/compliance/VMTest.php +++ b/test/compliance/VMTest.php @@ -146,6 +146,15 @@ public static function providePHPTests(): \Generator && !str_contains($name, 'crc32c_phantom')) { continue; } + if (!CompilerVersion::supportsHebrevc() + && str_contains($name, 'hebrevc') + && !str_contains($name, 'hebrevc_phantom')) { + continue; + } + if (CompilerVersion::supportsHebrevc() + && str_contains($name, 'hebrevc_phantom')) { + continue; + } if (!CompilerVersion::supportsJsonValidate() && str_contains($name, 'json_validate') && !str_contains($name, 'json_validate_phantom') diff --git a/test/compliance/cases/stdlib/hebrevc.phpt b/test/compliance/cases/stdlib/hebrevc.phpt index f96686e66ad..ff41cba1319 100644 --- a/test/compliance/cases/stdlib/hebrevc.phpt +++ b/test/compliance/cases/stdlib/hebrevc.phpt @@ -1,5 +1,7 @@ --TEST-- stdlib hebrevc() visual Hebrew with newline conversion (#17183) +--ENV-- +PHP_COMPILER_PROFILE=8.4 --FILE-- function_exists($fn) +); +echo [] === $leaked ? "ok\n" : 'fail: '.implode(',', $leaked)."\n"; +--EXPECT-- +ok diff --git a/test/repro/maintainer_gap_crc32c_builtin.php b/test/repro/maintainer_gap_crc32c_builtin.php index 8bcfea9bc51..f633ef26e3e 100644 --- a/test/repro/maintainer_gap_crc32c_builtin.php +++ b/test/repro/maintainer_gap_crc32c_builtin.php @@ -3,7 +3,9 @@ declare(strict_types=1); /** - * Maintainer repro: crc32c() enabled on default dev profile (#17139, ext/standard/crc32.c). + * Maintainer repro: crc32c() on forward profile (#17139, ext/standard/crc32.c). + * + * Run with: PHP_COMPILER_PROFILE=8.4 php bin/vm.php test/repro/maintainer_gap_crc32c_builtin.php */ if (!function_exists('crc32c')) { diff --git a/test/repro/maintainer_gap_forward_profile_builtin_leak_82.php b/test/repro/maintainer_gap_forward_profile_builtin_leak_82.php new file mode 100644 index 00000000000..5bac87b753a --- /dev/null +++ b/test/repro/maintainer_gap_forward_profile_builtin_leak_82.php @@ -0,0 +1,29 @@ +assertTrue(CompilerVersion::supportsPhp84ReflectionProbeBuiltins()); - $this->assertTrue(CompilerVersion::advertisesPhp84ReflectionProbeBuiltins()); + $this->assertFalse(CompilerVersion::supportsPhp84ReflectionProbeBuiltins()); + $this->assertFalse(CompilerVersion::advertisesPhp84ReflectionProbeBuiltins()); } finally { if (false === $prev) { putenv('PHP_COMPILER_PROFILE'); @@ -143,10 +143,10 @@ public function testClassUsesRecursiveWithheldOnReferenceProfile(): void $prev = getenv('PHP_COMPILER_PROFILE'); putenv('PHP_COMPILER_PROFILE'); try { - $this->assertTrue(CompilerVersion::supportsClassUsesRecursive()); - $this->assertTrue(CompilerVersion::advertisesClassUsesRecursive()); + $this->assertFalse(CompilerVersion::supportsClassUsesRecursive()); + $this->assertFalse(CompilerVersion::advertisesClassUsesRecursive()); $runtime = new Runtime(); - $this->assertTrue(isset($runtime->vmContext->functions['class_uses_recursive'])); + $this->assertFalse(isset($runtime->vmContext->functions['class_uses_recursive'])); } finally { if (false === $prev) { putenv('PHP_COMPILER_PROFILE'); @@ -540,15 +540,15 @@ public function testDisktotalspaceNotAdvertisedOnReferenceProfile(): void $this->assertFalse(CompilerVersion::supportsDisktotalspace()); } - public function testCrc32cAdvertisedOnDefaultDevProfile(): void + public function testCrc32cWithheldOnReferenceProfile(): void { $prev = getenv('PHP_COMPILER_PROFILE'); putenv('PHP_COMPILER_PROFILE'); try { - $this->assertTrue(CompilerVersion::supportsCrc32c()); - $this->assertTrue(CompilerVersion::advertisesCrc32c()); + $this->assertFalse(CompilerVersion::supportsCrc32c()); + $this->assertFalse(CompilerVersion::advertisesCrc32c()); $runtime = new Runtime(); - $this->assertTrue(isset($runtime->vmContext->functions['crc32c'])); + $this->assertFalse(isset($runtime->vmContext->functions['crc32c'])); } finally { if (false === $prev) { putenv('PHP_COMPILER_PROFILE'); @@ -582,13 +582,13 @@ public function testVmDoesNotRegisterZendThreadIdOnReferenceProfile(): void $this->assertFalse(isset($runtime->vmContext->functions['zend_thread_id'])); } - public function testVmRegistersClassUsesRecursiveOnDefaultDevProfile(): void + public function testVmDoesNotRegisterClassUsesRecursiveOnReferenceProfile(): void { $prev = getenv('PHP_COMPILER_PROFILE'); putenv('PHP_COMPILER_PROFILE'); $runtime = new Runtime(); try { - $this->assertTrue(isset($runtime->vmContext->functions['class_uses_recursive'])); + $this->assertFalse(isset($runtime->vmContext->functions['class_uses_recursive'])); } finally { if (false === $prev) { putenv('PHP_COMPILER_PROFILE'); @@ -598,7 +598,7 @@ public function testVmRegistersClassUsesRecursiveOnDefaultDevProfile(): void } } - public function testVmRegistersPhp84ReflectionProbeBuiltinsOnDefaultDevProfile(): void + public function testVmDoesNotRegisterPhp84ReflectionProbeBuiltinsOnReferenceProfile(): void { $prev = getenv('PHP_COMPILER_PROFILE'); putenv('PHP_COMPILER_PROFILE'); @@ -606,7 +606,7 @@ public function testVmRegistersPhp84ReflectionProbeBuiltinsOnDefaultDevProfile() $runtime = new Runtime(); $ctx = $runtime->vmContext; foreach (['attribute_exists', 'class_meth_exists', 'unitenum_exists'] as $fn) { - $this->assertTrue(isset($ctx->functions[$fn]), $fn); + $this->assertFalse(isset($ctx->functions[$fn]), $fn); } } finally { if (false === $prev) { @@ -808,13 +808,13 @@ public function testVmRegistersCrc32cOnForwardProfile(): void } } - public function testMbTrimEnabledOnDefaultDevProfile(): void + public function testMbTrimWithheldOnReferenceProfile(): void { $prev = getenv('PHP_COMPILER_PROFILE'); putenv('PHP_COMPILER_PROFILE'); try { - $this->assertTrue(CompilerVersion::supportsMbTrimFunctions()); - $this->assertTrue(CompilerVersion::advertisesMbTrimFunctions()); + $this->assertFalse(CompilerVersion::supportsMbTrimFunctions()); + $this->assertFalse(CompilerVersion::advertisesMbTrimFunctions()); } finally { if (false === $prev) { putenv('PHP_COMPILER_PROFILE'); @@ -840,7 +840,7 @@ public function testMbTrimWithheldOnPhp82Profile(): void } } - public function testVmRegistersMbTrimOnDefaultDevProfile(): void + public function testVmDoesNotRegisterMbTrimOnReferenceProfile(): void { $prev = getenv('PHP_COMPILER_PROFILE'); putenv('PHP_COMPILER_PROFILE'); @@ -848,7 +848,7 @@ public function testVmRegistersMbTrimOnDefaultDevProfile(): void $runtime = new Runtime(); $ctx = $runtime->vmContext; foreach (['mb_trim', 'mb_ltrim', 'mb_rtrim'] as $fn) { - $this->assertTrue(isset($ctx->functions[$fn]), $fn); + $this->assertFalse(isset($ctx->functions[$fn]), $fn); } } finally { if (false === $prev) { diff --git a/test/unit/CompilerVersionGateTest.php b/test/unit/CompilerVersionGateTest.php index 240725a1461..dcdc346744b 100644 --- a/test/unit/CompilerVersionGateTest.php +++ b/test/unit/CompilerVersionGateTest.php @@ -74,12 +74,12 @@ public function testSupportsClassHasFunctionsTrueOnForwardProfile(): void } } - public function testSupportsPhp84ReflectionProbeBuiltinsTrueOnDefaultDevProfile(): void + public function testSupportsPhp84ReflectionProbeBuiltinsFalseOnReferenceProfile(): void { $prev = getenv('PHP_COMPILER_PROFILE'); putenv('PHP_COMPILER_PROFILE'); try { - $this->assertTrue(CompilerVersion::supportsPhp84ReflectionProbeBuiltins()); + $this->assertFalse(CompilerVersion::supportsPhp84ReflectionProbeBuiltins()); } finally { if (false === $prev) { putenv('PHP_COMPILER_PROFILE'); @@ -1214,13 +1214,13 @@ public function testVmRegistersJsonValidateOnForwardProfile(): void } } - public function testSupportsMbTrimFunctionsEnabledOnDefaultDevProfile(): void + public function testSupportsMbTrimFunctionsWithheldOnReferenceProfile(): void { $prev = getenv('PHP_COMPILER_PROFILE'); putenv('PHP_COMPILER_PROFILE'); try { - $this->assertTrue(CompilerVersion::supportsMbTrimFunctions()); - $this->assertTrue(CompilerVersion::advertisesMbTrimFunctions()); + $this->assertFalse(CompilerVersion::supportsMbTrimFunctions()); + $this->assertFalse(CompilerVersion::advertisesMbTrimFunctions()); } finally { if (false === $prev) { putenv('PHP_COMPILER_PROFILE'); @@ -1246,7 +1246,7 @@ public function testSupportsMbTrimFunctionsWithheldOnPhp82Profile(): void } } - public function testVmRegistersMbTrimOnDefaultDevProfile(): void + public function testVmDoesNotRegisterMbTrimOnReferenceProfile(): void { $prev = getenv('PHP_COMPILER_PROFILE'); putenv('PHP_COMPILER_PROFILE'); @@ -1254,7 +1254,7 @@ public function testVmRegistersMbTrimOnDefaultDevProfile(): void $runtime = new Runtime(); $ctx = $runtime->vmContext; foreach (['mb_trim', 'mb_ltrim', 'mb_rtrim'] as $fn) { - $this->assertTrue(isset($ctx->functions[$fn]), $fn); + $this->assertFalse(isset($ctx->functions[$fn]), $fn); } } finally { if (false === $prev) {