From f710435413354e39ed2a007703e1fce58b194ed0 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 28 Sep 2026 09:25:19 +0800 Subject: [PATCH] fix(nano): preserve Windows runtime function table --- .github/workflows/windows-build.yml | 28 +++++++++++++++++ phpunit/src/EntryScriptCodegenTest.php | 1 + phpunit/src/NanoCapabilityPolicyTest.php | 11 ++++++- src/Translator.php | 39 ++++++++++++++++++++---- tests/windows/smoke/main.php | 9 +++++- 5 files changed, 80 insertions(+), 8 deletions(-) diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 1c6746d8..3d48ffe4 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -433,6 +433,34 @@ jobs: throw "Unexpected Windows Nano smoke output: $stdout" } + $policyInfo = [Diagnostics.ProcessStartInfo]::new() + $policyInfo.FileName = $nanoExe + $policyInfo.ArgumentList.Add('zts') + $policyInfo.ArgumentList.Add('nano') + $policyInfo.UseShellExecute = $false + $policyInfo.RedirectStandardOutput = $true + $policyInfo.RedirectStandardError = $true + + $policyProcess = [Diagnostics.Process]::new() + $policyProcess.StartInfo = $policyInfo + if (-not $policyProcess.Start()) { + throw 'Unable to start the Windows Nano policy probe' + } + $policyStdout = $policyProcess.StandardOutput.ReadToEnd() + $policyStderr = $policyProcess.StandardError.ReadToEnd() + $policyProcess.WaitForExit() + + Write-Host "Windows Nano policy stdout: $policyStdout" + Write-Host "Windows Nano policy stderr: $policyStderr" + Write-Host "Windows Nano policy exit code: $($policyProcess.ExitCode)" + if ($policyProcess.ExitCode -eq 0) { + throw 'Windows Nano policy probe unexpectedly succeeded' + } + $policyOutput = "$policyStdout`n$policyStderr" + if (-not $policyOutput.Contains('Function `exec` is not supported in nano mode')) { + throw "Windows Nano policy probe returned an unexpected error: $policyOutput" + } + - name: Package tested Windows compiler if: startsWith(github.ref, 'refs/tags/') shell: pwsh diff --git a/phpunit/src/EntryScriptCodegenTest.php b/phpunit/src/EntryScriptCodegenTest.php index 9d69c439..b7d2dca3 100644 --- a/phpunit/src/EntryScriptCodegenTest.php +++ b/phpunit/src/EntryScriptCodegenTest.php @@ -49,6 +49,7 @@ PHP, ); self::assertStringNotContainsString('php::eval("\\n', $extension); self::assertStringNotContainsString('zend_disable_functions(', $extension); + self::assertStringNotContainsString('typephp_disable_nano_function(', $extension); } public function testNanoEntrypointForwardsArgcAndArgvWithTheSharedContract(): void diff --git a/phpunit/src/NanoCapabilityPolicyTest.php b/phpunit/src/NanoCapabilityPolicyTest.php index 5392a0cf..be66de23 100644 --- a/phpunit/src/NanoCapabilityPolicyTest.php +++ b/phpunit/src/NanoCapabilityPolicyTest.php @@ -156,9 +156,18 @@ final class NanoCapabilityPolicyTest extends BaseTest self::assertStringContainsString('php_main();', $extension); self::assertStringNotContainsString('php::eval(', $extension); self::assertStringContainsString( - 'zend_disable_functions("exec,passthru,pcntl_exec,popen,proc_close,proc_get_status,proc_nice,proc_open,proc_terminate,shell_exec,system")', + 'typephp_disable_nano_function("exec", 4);', $extension, ); + self::assertStringContainsString( + 'function->internal_function.handler = typephp_nano_disabled_function;', + $extension, + ); + self::assertStringContainsString( + 'static void ZEND_FASTCALL typephp_nano_disabled_function(INTERNAL_FUNCTION_PARAMETERS)', + $extension, + ); + self::assertStringNotContainsString('zend_disable_functions(', $extension); self::assertStringContainsString('_SERVER.item("SCRIPT_FILENAME", true)', $extension); } finally { $translator = $previousTranslator; diff --git a/src/Translator.php b/src/Translator.php index 021a5229..0487aa80 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1835,6 +1835,30 @@ CODE; // module_clean end $moduleName = $this->getModuleName(); + $installNanoPolicyHandlers = $this->isNanoPolicyMode() + && !$this->isNanoMode() + && $this->isBuildModeBin() + && $this->hasSapi('embed'); + if ($installNanoPolicyHandlers) { + // Windows Nano uses the full PHP runtime. Keep forbidden process + // functions in Zend's persistent table so shutdown boundaries stay + // intact, but replace their handlers before generated code runs. + // This path is binary/embed-only and executes once per process. + $code .= <<<'CODE' +static void ZEND_FASTCALL typephp_nano_disabled_function(INTERNAL_FUNCTION_PARAMETERS) { + const zend_string *name = EX(func)->common.function_name; + zend_throw_error(nullptr, "Function `%s` is not supported in nano mode", name ? ZSTR_VAL(name) : "unknown"); +} + +static void typephp_disable_nano_function(const char *name, size_t name_length) { + auto *function = static_cast(zend_hash_str_find_ptr(EG(function_table), name, name_length)); + if (function != nullptr && function->type == ZEND_INTERNAL_FUNCTION) { + function->internal_function.handler = typephp_nano_disabled_function; + } +} + +CODE; + } // rinit begin $code .= 'PHP_RINIT_FUNCTION(' . $moduleName . ') {' . PHP_EOL; $code .= 'if (UNEXPECTED(php_request_cache != nullptr)) {' . PHP_EOL; @@ -1851,14 +1875,17 @@ CODE; $code .= $this->getIndent() . 'return FAILURE;' . PHP_EOL; $code .= '}' . PHP_EOL; $code .= 'php::request_init();' . PHP_EOL; - if ($this->isNanoPolicyMode() && !$this->isNanoMode()) { + if ($installNanoPolicyHandlers) { // The full Windows runtime still contains standard/process modules. - // Remove command functions from Zend's table after every module has + // Block command functions after every module has // started so variable functions and call_user_func cannot bypass - // the compile-time named-call check. - $code .= 'zend_disable_functions(' - . $this->genCharPtr($this->getNanoPolicyDisabledFunctionList(), true) - . ');' . PHP_EOL; + // the compile-time named-call check. Replacing handlers preserves + // Zend's persistent function-table layout for embed shutdown. + foreach (explode(',', $this->getNanoPolicyDisabledFunctionList()) as $functionName) { + $functionArg = $this->genCharPtr($functionName, true); + $code .= 'typephp_disable_nano_function(' . $functionArg . ', ' + . strlen($functionName) . ');' . PHP_EOL; + } } $code .= 'module_init();' . PHP_EOL; diff --git a/tests/windows/smoke/main.php b/tests/windows/smoke/main.php index 5d513e09..7bba5853 100644 --- a/tests/windows/smoke/main.php +++ b/tests/windows/smoke/main.php @@ -9,7 +9,7 @@ function requireWindowsCondition(bool $condition, string $message): void function main(int $argc, array $argv): void { - requireWindowsCondition($argc === 2, 'Expected the thread-safety mode argument'); + requireWindowsCondition($argc === 2 || $argc === 3, 'Expected the thread-safety mode argument'); $expectedZts = $argv[1] === 'zts'; requireWindowsCondition(PHP_OS_FAMILY === 'Windows', 'Expected PHP_OS_FAMILY=Windows'); @@ -30,5 +30,12 @@ function main(int $argc, array $argv): void requireWindowsCondition(file_get_contents($path) === 'windows-file-api', 'Windows file read failed'); requireWindowsCondition(unlink($path), 'Windows file cleanup failed'); + if ($argc === 3) { + requireWindowsCondition($argv[2] === 'nano', 'Unexpected Windows smoke mode'); + $blockedFunction = 'exec'; + call_user_func($blockedFunction, 'echo typephp-nano-policy-bypass'); + requireWindowsCondition(false, 'Nano dynamic call reached exec()'); + } + echo 'windows-smoke-ok:', $expectedZts ? 'zts' : 'nts'; }