From 37303fa9495c86b012d0ef0e91911d1114224c5b Mon Sep 17 00:00:00 2001 From: PurHur Date: Tue, 18 Aug 2026 10:04:45 +0000 Subject: [PATCH] Stdlib: honor allow_url_include for script-style data:// opens (#32104) php_strip_whitespace(), highlight_file(), and show_source() now match php-src by refusing URL wrappers when allow_url_include is off, while file_get_contents() keeps using allow_url_fopen. Co-authored-by: Cursor --- ext/standard/JitHighlight.php | 33 ++++ ext/standard/JitStreamIncludeOpen.php | 142 +++++++++++++++++ ext/standard/JitStripWhitespace.php | 38 +++-- ext/standard/StreamIncludeOpenJitHelper.php | 26 ++++ ext/standard/VmStreamIncludeOpenPolicy.php | 146 ++++++++++++++++++ ext/standard/highlight_file.php | 9 ++ ext/standard/php_strip_whitespace.php | 6 + lib/JIT/Builtin/StreamIncludeOpen.php | 38 +++++ .../allow_url_include_script_open_data.phpt | 23 +++ .../maintainer_gap_highlight_file_data.php | 13 ++ .../maintainer_gap_strip_whitespace_data.php | 14 ++ test/unit/StreamIncludeOpenPolicyTest.php | 41 +++++ 12 files changed, 515 insertions(+), 14 deletions(-) create mode 100644 ext/standard/JitStreamIncludeOpen.php create mode 100644 ext/standard/StreamIncludeOpenJitHelper.php create mode 100644 ext/standard/VmStreamIncludeOpenPolicy.php create mode 100644 lib/JIT/Builtin/StreamIncludeOpen.php create mode 100644 test/compliance/cases/stdlib/allow_url_include_script_open_data.phpt create mode 100644 test/repro/maintainer_gap_highlight_file_data.php create mode 100644 test/repro/maintainer_gap_strip_whitespace_data.php create mode 100644 test/unit/StreamIncludeOpenPolicyTest.php diff --git a/ext/standard/JitHighlight.php b/ext/standard/JitHighlight.php index 40d40951bec..c4f4368f188 100644 --- a/ext/standard/JitHighlight.php +++ b/ext/standard/JitHighlight.php @@ -82,11 +82,44 @@ public static function highlightFile(Context $context, string $functionName, JIT // Arity guarded by highlight_file/show_source::call via requireArgCountRangeJit (#30689). $argc = \count($args); + $pathLit = $args[0]->compileTimeString ?? null; + $blockedEarly = JitStreamIncludeOpen::rejectCompileTimeBlockedScriptOpen( + $context, + $pathLit, + $functionName, + true, + true + ); + if (null !== $blockedEarly) { + return $blockedEarly; + } + // Z_PARAM_PATH then empty-path: Zend E_WARNING then ValueError (#30514). $pathStr = JitStringBuiltinArg::lowerPath($context, $args[0], $functionName, 0, 'filename'); self::rejectEmptyPathWithHighlightWarning($context, $args[0], $pathStr, $functionName); StringFileGetContents::implement($context); JitNativeString::ensureInsertBlock($context); + + return JitStreamIncludeOpen::wrapWithRuntimeBlockedGuard( + $context, + $pathStr, + $functionName, + true, + static fn (Context $ctx): Value => self::emitMissingFileResult($ctx, $args, $argc, $functionName), + static fn (Context $ctx): Value => self::lowerReadAndHighlight($ctx, $pathStr, $args, $argc, $functionName) + ); + } + + /** + * @param list $args + */ + private static function lowerReadAndHighlight( + Context $context, + Value $pathStr, + array $args, + int $argc, + string $functionName + ): Value { $contents = $context->builder->call( $context->lookupFunction('__compiler_file_get_contents'), $pathStr diff --git a/ext/standard/JitStreamIncludeOpen.php b/ext/standard/JitStreamIncludeOpen.php new file mode 100644 index 00000000000..645393237c7 --- /dev/null +++ b/ext/standard/JitStreamIncludeOpen.php @@ -0,0 +1,142 @@ +getTypeFromString('i1'); + $i32 = $context->getTypeFromString('int32'); + $blocked = $context->builder->call( + StreamIncludeOpen::helperFunction($context), + $pathStr, + $context->builder->load($context->constantStringFromString($function)), + $i32->constInt($forHighlight ? 1 : 0, false) + ); + $isBlocked = $context->builder->icmp(Builder::INT_NE, $blocked, $i1->constInt(0, false)); + $blockedBb = BasicBlockHelper::append($context, $function.'_url_include_blocked'); + $okBb = BasicBlockHelper::append($context, $function.'_url_include_ok'); + $doneBb = BasicBlockHelper::append($context, $function.'_url_include_done'); + $context->builder->branchIf($isBlocked, $blockedBb, $okBb); + + $context->builder->positionAtEnd($blockedBb); + $blockedVal = $makeBlockedReturn($context); + $blockedEnd = $context->builder->getInsertBlock(); + $context->builder->branch($doneBb); + + $context->builder->positionAtEnd($okBb); + $okVal = $continue($context); + $okEnd = $context->builder->getInsertBlock(); + $context->builder->branch($doneBb); + + $context->builder->positionAtEnd($doneBb); + $ptrTy = $context->getTypeFromString('__value__*'); + $phi = $context->builder->phi($ptrTy); + $phi->addIncoming($blockedVal, $blockedEnd); + $phi->addIncoming($okVal, $okEnd); + + return $phi; + } + + public static function materializeEmptyString(Context $context): Value + { + $slot = JitValueBox::alloc($context); + $ptr = JitValueBox::pointer($context, $slot); + $owned = $context->builder->call( + $context->lookupFunction('__string__separate'), + $context->builder->load($context->constantStringFromString('')) + ); + $context->builder->call($context->lookupFunction('__value__writeString'), $ptr, $owned); + + return $ptr; + } + + public static function materializeFalse(Context $context): Value + { + $slot = JitValueBox::alloc($context); + $ptr = JitValueBox::pointer($context, $slot); + JitValueBox::writeBool($context, $slot, $context->constantFromBool(false)); + + return $ptr; + } + + private static function emitCompileTimeBlockedWarnings( + Context $context, + string $function, + string $path, + bool $forHighlight + ): void { + self::emitWarning($context, VmStreamIncludeOpenPolicy::wrapperDisabledMessage($function, $path)); + self::emitWarning($context, VmStreamIncludeOpenPolicy::failedToOpenMessage($function, $path)); + if ($forHighlight) { + self::emitWarning( + $context, + VmStreamOpenFailure::highlightFailedOpeningMessage($function, $path) + ); + } + } + + private static function emitWarning(Context $context, string $message): void + { + StringTriggerError::ensureLinked($context); + $i8p = $context->getTypeFromString('int8*'); + $i32 = $context->getTypeFromString('int32'); + $sizeT = $context->getTypeFromString('size_t'); + $msgPtr = $context->builder->pointerCast($context->constantFromString($message), $i8p); + $emptyFile = $context->builder->pointerCast($context->constantFromString(''), $i8p); + $context->builder->call( + $context->lookupFunction('__compiler_trigger_error'), + $msgPtr, + $sizeT->constInt(\strlen($message), false), + $i32->constInt(ErrorReporter::E_WARNING, false), + $emptyFile, + $i32->constInt(0, false) + ); + } +} diff --git a/ext/standard/JitStripWhitespace.php b/ext/standard/JitStripWhitespace.php index 85e41a268b8..9239f8ed9e9 100644 --- a/ext/standard/JitStripWhitespace.php +++ b/ext/standard/JitStripWhitespace.php @@ -25,6 +25,17 @@ public static function invoke(Context $context, JITVariable ...$args): Value } $pathLit = $args[0]->compileTimeString ?? null; + $blockedEarly = JitStreamIncludeOpen::rejectCompileTimeBlockedScriptOpen( + $context, + $pathLit, + 'php_strip_whitespace', + false, + false + ); + if (null !== $blockedEarly) { + return $blockedEarly; + } + if (null !== $pathLit) { if ('' === $pathLit) { throw new \ValueError(PathSupport::EMPTY_PATH_VALUE_ERROR_MESSAGE); @@ -41,8 +52,20 @@ public static function invoke(Context $context, JITVariable ...$args): Value StripWhitespace::ensureLinked($context); StringFileGetContents::implement($context); - $pathStr = JitStreamPath::lowerNonEmptyPath($context, $args[0], 'php_strip_whitespace', 0, 'filename'); + + return JitStreamIncludeOpen::wrapWithRuntimeBlockedGuard( + $context, + $pathStr, + 'php_strip_whitespace', + false, + static fn (Context $ctx): Value => JitStreamIncludeOpen::materializeEmptyString($ctx), + static fn (Context $ctx): Value => self::lowerReadAndStrip($ctx, $pathStr) + ); + } + + private static function lowerReadAndStrip(Context $context, Value $pathStr): Value + { $contents = $context->builder->call( $context->lookupFunction('__compiler_file_get_contents'), $pathStr @@ -76,19 +99,6 @@ public static function invoke(Context $context, JITVariable ...$args): Value return $ptr; } - private static function emptyString(Context $context): Value - { - $slot = JitValueBox::alloc($context); - $ptr = JitValueBox::pointer($context, $slot); - $context->builder->call( - $context->lookupFunction('__value__writeString'), - $ptr, - self::emptyOwnedString($context) - ); - - return $ptr; - } - private static function emptyOwnedString(Context $context): Value { return $context->builder->call( diff --git a/ext/standard/StreamIncludeOpenJitHelper.php b/ext/standard/StreamIncludeOpenJitHelper.php new file mode 100644 index 00000000000..d7dd248f135 --- /dev/null +++ b/ext/standard/StreamIncludeOpenJitHelper.php @@ -0,0 +1,26 @@ +vmContext) { + return; + } + self::emitWarnings( + $frame->vmContext, + $function, + $path, + $forHighlight, + $frame->scriptPath, + $frame + ); + } + + public static function warnScriptOpenBlockedStandalone( + string $function, + string $path, + bool $forHighlight = false + ): void { + $vm = \PHPCompiler\VM::running(); + $ctx = null !== $vm ? $vm->context : null; + if (null === $ctx) { + return; + } + self::emitWarnings($ctx, $function, $path, $forHighlight, $ctx->scriptStack->current(), null); + } + + private static function emitWarnings( + Context $ctx, + string $function, + string $path, + bool $forHighlight, + ?string $scriptFile, + ?Frame $frame + ): void { + $file = \is_string($scriptFile) && '' !== $scriptFile ? $scriptFile : null; + $ctx->errors->triggerError( + self::wrapperDisabledMessage($function, $path), + ErrorReporter::E_WARNING, + $file, + $ctx, + $frame + ); + $ctx->errors->triggerError( + self::failedToOpenMessage($function, $path), + ErrorReporter::E_WARNING, + $file, + $ctx, + $frame + ); + if ($forHighlight) { + $ctx->errors->triggerError( + VmStreamOpenFailure::highlightFailedOpeningMessage($function, $path), + ErrorReporter::E_WARNING, + $file, + $ctx, + $frame + ); + } + } + + private static function schemeLabel(string $path): string + { + $protocol = VmStreamWrapperRegistry::parseProtocol($path); + + return null !== $protocol ? $protocol.'://' : 'url://'; + } +} diff --git a/ext/standard/highlight_file.php b/ext/standard/highlight_file.php index dae11e2f505..047c084b52c 100644 --- a/ext/standard/highlight_file.php +++ b/ext/standard/highlight_file.php @@ -53,6 +53,15 @@ public static function run(Frame $frame, string $functionName): void VmStreamOpenFailure::warnHighlightFailedOpening($frame, $functionName, $path); throw new \ValueError(PathSupport::EMPTY_PATH_VALUE_ERROR_MESSAGE); } + if (VmStreamIncludeOpenPolicy::blockedForScriptOpen($path, $frame->vmContext)) { + VmStreamIncludeOpenPolicy::warnScriptOpenBlocked($frame, $functionName, $path, true); + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->bool(false); + + return; + } $contents = VmFs::readPathContentsViaOpen($path, $frame->vmContext); if (false === $contents) { VmStreamOpenFailure::warnFailedToOpen($frame, $functionName, $path); diff --git a/ext/standard/php_strip_whitespace.php b/ext/standard/php_strip_whitespace.php index f608ea729dd..d6c13a2978b 100644 --- a/ext/standard/php_strip_whitespace.php +++ b/ext/standard/php_strip_whitespace.php @@ -39,6 +39,12 @@ public function execute(Frame $frame): void 'php_strip_whitespace', 'filename' ); + if (VmStreamIncludeOpenPolicy::blockedForScriptOpen($path, $frame->vmContext)) { + VmStreamIncludeOpenPolicy::warnScriptOpenBlocked($frame, 'php_strip_whitespace', $path); + $frame->returnVar->string(''); + + return; + } $contents = VmFs::fileGetContents($path); if (false === $contents) { VmStreamOpenFailure::warnFailedToOpen($frame, 'php_strip_whitespace', $path); diff --git a/lib/JIT/Builtin/StreamIncludeOpen.php b/lib/JIT/Builtin/StreamIncludeOpen.php new file mode 100644 index 00000000000..e9e0415ba82 --- /dev/null +++ b/lib/JIT/Builtin/StreamIncludeOpen.php @@ -0,0 +1,38 @@ + */ + private const COMPILED_HELPERS = [ + self::HELPER_LOGICAL, + ]; + + public static function ensureLinked(Context $context): void + { + JitVmHelperLink::ensureCompiled( + $context, + self::HELPER_PATH, + self::COMPILED_HELPERS, + '#32104' + ); + } + + public static function helperFunction(Context $context): \PHPLLVM\Value\Function_ + { + self::ensureLinked($context); + + return JitVmHelperLink::lookupCompiled($context, self::HELPER_LOGICAL, '#32104'); + } +} diff --git a/test/compliance/cases/stdlib/allow_url_include_script_open_data.phpt b/test/compliance/cases/stdlib/allow_url_include_script_open_data.phpt new file mode 100644 index 00000000000..e625e5f59f6 --- /dev/null +++ b/test/compliance/cases/stdlib/allow_url_include_script_open_data.phpt @@ -0,0 +1,23 @@ +--TEST-- +Issue #32104 — php_strip_whitespace/highlight_file honor allow_url_include for data:// +--FILE-- + +--EXPECT-- +Warning: php_strip_whitespace(): data:// wrapper is disabled in the server configuration by allow_url_include=0 in %s on line %d +Warning: php_strip_whitespace(data://text/plain,assertTrue(VmStreamIncludeOpenPolicy::isUrlWrapper('data://text/plain,x')); + $this->assertFalse(VmStreamIncludeOpenPolicy::isUrlWrapper('/tmp/foo.php')); + $this->assertFalse(VmStreamIncludeOpenPolicy::isUrlWrapper('relative.php')); + } + + public function testBlockedForScriptOpenWhenAllowUrlIncludeOff(): void + { + $this->assertTrue( + VmStreamIncludeOpenPolicy::blockedForScriptOpen('data://text/plain,x', null) + ); + $this->assertFalse( + VmStreamIncludeOpenPolicy::blockedForScriptOpen('/etc/passwd', null) + ); + } + + public function testWrapperDisabledMessageMatchesZendShape(): void + { + $msg = VmStreamIncludeOpenPolicy::wrapperDisabledMessage( + 'php_strip_whitespace', + 'data://text/plain,x' + ); + $this->assertSame( + 'php_strip_whitespace(): data:// wrapper is disabled in the server configuration by allow_url_include=0', + $msg + ); + } +}