diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0bd7549a..2afe1995 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -744,6 +744,7 @@ phutil_register_library_map(array( 'PhutilEnglishCanadaLocale' => 'internationalization/locales/PhutilEnglishCanadaLocale.php', 'PhutilErrorHandler' => 'error/PhutilErrorHandler.php', 'PhutilErrorHandlerTestCase' => 'error/__tests__/PhutilErrorHandlerTestCase.php', + 'PhutilErrorLog' => 'filesystem/PhutilErrorLog.php', 'PhutilErrorTrap' => 'error/PhutilErrorTrap.php', 'PhutilEvent' => 'events/PhutilEvent.php', 'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php', @@ -849,6 +850,7 @@ phutil_register_library_map(array( 'PhutilRawEnglishLocale' => 'internationalization/locales/PhutilRawEnglishLocale.php', 'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php', 'PhutilReadableSerializerTestCase' => 'readableserializer/__tests__/PhutilReadableSerializerTestCase.php', + 'PhutilRegexException' => 'exception/PhutilRegexException.php', 'PhutilRope' => 'utils/PhutilRope.php', 'PhutilRopeTestCase' => 'utils/__tests__/PhutilRopeTestCase.php', 'PhutilServiceProfiler' => 'serviceprofiler/PhutilServiceProfiler.php', @@ -1009,6 +1011,9 @@ phutil_register_library_map(array( 'phutil_partition' => 'utils/utils.php', 'phutil_passthru' => 'future/exec/execx.php', 'phutil_person' => 'internationalization/pht.php', + 'phutil_preg_match' => 'utils/utils.php', + 'phutil_preg_match_all' => 'utils/utils.php', + 'phutil_raise_preg_exception' => 'utils/utils.php', 'phutil_register_library' => 'init/lib/core.php', 'phutil_register_library_map' => 'init/lib/core.php', 'phutil_set_system_locale' => 'utils/utf8.php', @@ -1812,6 +1817,7 @@ phutil_register_library_map(array( 'PhutilEnglishCanadaLocale' => 'PhutilLocale', 'PhutilErrorHandler' => 'Phobject', 'PhutilErrorHandlerTestCase' => 'PhutilTestCase', + 'PhutilErrorLog' => 'Phobject', 'PhutilErrorTrap' => 'Phobject', 'PhutilEvent' => 'Phobject', 'PhutilEventConstants' => 'Phobject', @@ -1925,6 +1931,7 @@ phutil_register_library_map(array( 'PhutilRawEnglishLocale' => 'PhutilLocale', 'PhutilReadableSerializer' => 'Phobject', 'PhutilReadableSerializerTestCase' => 'PhutilTestCase', + 'PhutilRegexException' => 'Exception', 'PhutilRope' => 'Phobject', 'PhutilRopeTestCase' => 'PhutilTestCase', 'PhutilServiceProfiler' => 'Phobject', diff --git a/src/exception/PhutilRegexException.php b/src/exception/PhutilRegexException.php new file mode 100644 index 00000000..90b1b3bf --- /dev/null +++ b/src/exception/PhutilRegexException.php @@ -0,0 +1,3 @@ +logName = $log_name; + return $this; + } + + public function getLogName() { + return $this->logName; + } + + public function setLogPath($log_path) { + $this->logPath = $log_path; + return $this; + } + + public function getLogPath() { + return $this->logPath; + } + + public function activateLog() { + $log_path = $this->getLogPath(); + + if ($log_path !== null) { + // Test that the path is writable. + $write_exception = null; + try { + Filesystem::assertWritableFile($log_path); + } catch (FilesystemException $ex) { + $write_exception = $ex; + } + + // If we hit an exception, try to create the containing directory. + if ($write_exception) { + $log_dir = dirname($log_path); + if (!Filesystem::pathExists($log_dir)) { + try { + Filesystem::createDirectory($log_dir, 0755, true); + } catch (FilesystemException $ex) { + throw new PhutilProxyException( + pht( + 'Unable to write log "%s" to path "%s". The containing '. + 'directory ("%s") does not exist or is not readable, and '. + 'could not be created.', + $this->getLogName(), + $log_path, + $log_dir), + $ex); + } + } + + // If we created the parent directory, test if the path is writable + // again. + try { + Filesystem::assertWritableFile($log_path); + $write_exception = null; + } catch (FilesystemException $ex) { + $write_exception = $ex; + } + } + + // If we ran into a write exception and couldn't resolve it, fail. + if ($write_exception) { + throw new PhutilProxyException( + pht( + 'Unable to write log "%s" to path "%s" because the path is not '. + 'writable.', + $this->getLogName(), + $log_path), + $write_exception); + } + } + + ini_set('error_log', $log_path); + PhutilErrorHandler::setErrorListener(array($this, 'onError')); + } + + public function onError($event, $value, array $metadata) { + // If we've set "error_log" to a real file, so messages won't be output to + // stderr by default. Copy them to stderr. + + if ($this->logPath === null) { + return; + } + + $message = idx($metadata, 'default_message'); + + if (strlen($message)) { + $message = tsprintf("%B\n", $message); + @fwrite(STDERR, $message); + } + } + +} diff --git a/src/utils/utils.php b/src/utils/utils.php index 3ae606d7..cbc4d5fb 100644 --- a/src/utils/utils.php +++ b/src/utils/utils.php @@ -2008,3 +2008,89 @@ function phutil_partition(array $map) { return $partitions; } + +function phutil_preg_match( + $pattern, + $subject, + $flags = 0, + $offset = 0) { + + $matches = null; + $result = @preg_match($pattern, $subject, $matches, $flags, $offset); + if ($result === false || $result === null) { + phutil_raise_preg_exception( + 'preg_match', + array( + $pattern, + $subject, + $matches, + $flags, + $offset, + )); + } + + return $matches; +} + +function phutil_preg_match_all( + $pattern, + $subject, + $flags = 0, + $offset = 0) { + + $matches = null; + $result = @preg_match_all($pattern, $subject, $matches, $flags, $offset); + if ($result === false || $result === null) { + phutil_raise_preg_exception( + 'preg_match_all', + array( + $pattern, + $subject, + $matches, + $flags, + $offset, + )); + } + + return $matches; +} + +function phutil_raise_preg_exception($function, array $argv) { + $trap = new PhutilErrorTrap(); + + // NOTE: This ugly construction to avoid issues with reference behavior when + // passing values through "call_user_func_array()". + + switch ($function) { + case 'preg_match': + @preg_match($argv[0], $argv[1], $argv[2], $argv[3], $argv[4]); + break; + case 'preg_match_all': + @preg_match_all($argv[0], $argv[1], $argv[2], $argv[3], $argv[4]); + break; + } + $error_message = $trap->getErrorsAsString(); + + $trap->destroy(); + + $pattern = $argv[0]; + $pattern_display = sprintf( + '"%s"', + addcslashes($pattern, '\\\"')); + + $message = array(); + $message[] = pht( + 'Call to %s(%s, ...) failed.', + $function, + $pattern_display); + + if (strlen($error_message)) { + $message[] = pht( + 'Regular expression engine emitted message: %s', + $error_message); + } + + $message = implode("\n\n", $message); + + throw new PhutilRegexException($message); +} diff --git a/support/init/init-script.php b/support/init/init-script.php index 9b67daef..ad40b8bd 100644 --- a/support/init/init-script.php +++ b/support/init/init-script.php @@ -56,6 +56,15 @@ function __arcanist_init_script__() { // inspect "args", and this option generally obscures useful debugging // information without any benefit in the context of Phabricator. 'zend.exception_ignore_args' => 0, + + // See T13100. We'd like the regex engine to fail, rather than segfault, + // if handed a pathological regular expression. + 'pcre.backtrack_limit' => 10000, + 'pcre.recusion_limit' => 10000, + + // NOTE: Phabricator applies a similar set of startup options for Web + // environments in "PhabricatorStartup". Changes here may also be + // appropriate to apply there. ); foreach ($config_map as $config_key => $config_value) { @@ -106,8 +115,6 @@ function __arcanist_init_script__() { PhutilErrorHandler::initialize(); - PhutilErrorHandler::initialize(); - // If "variables_order" excludes "E", silently repair it so that $_ENV has // the values we expect. PhutilExecutionEnvironment::repairMissingVariablesOrder();