fix(nano): preserve Windows runtime function table

master
韩天峰 2 weeks ago
parent e088776d24
commit f710435413
  1. 28
      .github/workflows/windows-build.yml
  2. 1
      phpunit/src/EntryScriptCodegenTest.php
  3. 11
      phpunit/src/NanoCapabilityPolicyTest.php
  4. 39
      src/Translator.php
  5. 9
      tests/windows/smoke/main.php

@ -433,6 +433,34 @@ jobs:
throw "Unexpected Windows Nano smoke output: $stdout" 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 - name: Package tested Windows compiler
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
shell: pwsh shell: pwsh

@ -49,6 +49,7 @@ PHP,
); );
self::assertStringNotContainsString('php::eval("\\n', $extension); self::assertStringNotContainsString('php::eval("\\n', $extension);
self::assertStringNotContainsString('zend_disable_functions(', $extension); self::assertStringNotContainsString('zend_disable_functions(', $extension);
self::assertStringNotContainsString('typephp_disable_nano_function(', $extension);
} }
public function testNanoEntrypointForwardsArgcAndArgvWithTheSharedContract(): void public function testNanoEntrypointForwardsArgcAndArgvWithTheSharedContract(): void

@ -156,9 +156,18 @@ final class NanoCapabilityPolicyTest extends BaseTest
self::assertStringContainsString('php_main();', $extension); self::assertStringContainsString('php_main();', $extension);
self::assertStringNotContainsString('php::eval(', $extension); self::assertStringNotContainsString('php::eval(', $extension);
self::assertStringContainsString( 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, $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); self::assertStringContainsString('_SERVER.item("SCRIPT_FILENAME", true)', $extension);
} finally { } finally {
$translator = $previousTranslator; $translator = $previousTranslator;

@ -1835,6 +1835,30 @@ CODE;
// module_clean end // module_clean end
$moduleName = $this->getModuleName(); $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_function *>(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 // rinit begin
$code .= 'PHP_RINIT_FUNCTION(' . $moduleName . ') {' . PHP_EOL; $code .= 'PHP_RINIT_FUNCTION(' . $moduleName . ') {' . PHP_EOL;
$code .= 'if (UNEXPECTED(php_request_cache != nullptr)) {' . PHP_EOL; $code .= 'if (UNEXPECTED(php_request_cache != nullptr)) {' . PHP_EOL;
@ -1851,14 +1875,17 @@ CODE;
$code .= $this->getIndent() . 'return FAILURE;' . PHP_EOL; $code .= $this->getIndent() . 'return FAILURE;' . PHP_EOL;
$code .= '}' . PHP_EOL; $code .= '}' . PHP_EOL;
$code .= 'php::request_init();' . PHP_EOL; $code .= 'php::request_init();' . PHP_EOL;
if ($this->isNanoPolicyMode() && !$this->isNanoMode()) { if ($installNanoPolicyHandlers) {
// The full Windows runtime still contains standard/process modules. // 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 // started so variable functions and call_user_func cannot bypass
// the compile-time named-call check. // the compile-time named-call check. Replacing handlers preserves
$code .= 'zend_disable_functions(' // Zend's persistent function-table layout for embed shutdown.
. $this->genCharPtr($this->getNanoPolicyDisabledFunctionList(), true) foreach (explode(',', $this->getNanoPolicyDisabledFunctionList()) as $functionName) {
. ');' . PHP_EOL; $functionArg = $this->genCharPtr($functionName, true);
$code .= 'typephp_disable_nano_function(' . $functionArg . ', '
. strlen($functionName) . ');' . PHP_EOL;
}
} }
$code .= 'module_init();' . PHP_EOL; $code .= 'module_init();' . PHP_EOL;

@ -9,7 +9,7 @@ function requireWindowsCondition(bool $condition, string $message): void
function main(int $argc, array $argv): 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'; $expectedZts = $argv[1] === 'zts';
requireWindowsCondition(PHP_OS_FAMILY === 'Windows', 'Expected PHP_OS_FAMILY=Windows'); 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(file_get_contents($path) === 'windows-file-api', 'Windows file read failed');
requireWindowsCondition(unlink($path), 'Windows file cleanup 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'; echo 'windows-smoke-ok:', $expectedZts ? 'zts' : 'nts';
} }

Loading…
Cancel
Save