diff --git a/ext/standard/JitTempnam.php b/ext/standard/JitTempnam.php index c4beb5185b1..f3d4be54362 100644 --- a/ext/standard/JitTempnam.php +++ b/ext/standard/JitTempnam.php @@ -10,7 +10,7 @@ use PHPLLVM\Builder; use PHPLLVM\Value; -/** LLVM lowering for tempnam() via __compiler_tempnam (issue #1201). */ +/** LLVM lowering for tempnam() via __compiler_tempnam (issue #1201, #4401). */ final class JitTempnam { /** @return Value */ diff --git a/ext/standard/VmFs.php b/ext/standard/VmFs.php index d9f20d50a9b..57521d707be 100644 --- a/ext/standard/VmFs.php +++ b/ext/standard/VmFs.php @@ -1192,13 +1192,19 @@ public static function rewind(int $handle): bool return 0 === @\fseek($fp, 0, \SEEK_SET); } - public static function tempnam(string $directory, string $prefix) { - $path = @\tempnam($directory, $prefix); - if (false === $path) { + public static function tempnam(string $directory, string $prefix) + { + $pfx = VmFsTempnam::normalizePrefix($prefix); + $path = VmFsTempnamNative::mkstemp($directory, $pfx); + if (false !== $path) { + return $path; + } + if (!\is_dir($directory) || !\is_writable($directory)) { return false; } + $path = \tempnam($directory, $pfx); - return $path; + return false === $path ? false : $path; } /** diff --git a/ext/standard/VmFsTempnam.php b/ext/standard/VmFsTempnam.php new file mode 100644 index 00000000000..a1db6c4a827 --- /dev/null +++ b/ext/standard/VmFsTempnam.php @@ -0,0 +1,77 @@ += self::PREFIX_MAX) { + return \substr($base, 0, self::PREFIX_MAX - 1); + } + + return $base; + } + + public static function invoke(string $directory, string $prefix, Frame $frame): string|false + { + $pfx = self::normalizePrefix($prefix); + $path = self::tryCreate($directory, $pfx); + if (false !== $path) { + return $path; + } + self::emitNotice($frame); + $fallback = \sys_get_temp_dir(); + if ('' === $fallback) { + return false; + } + + return self::tryCreate($fallback, $pfx); + } + + private static function tryCreate(string $dir, string $prefix): string|false + { + if ('' === $dir || '' === $prefix) { + return false; + } + $ffiPath = VmFsTempnamNative::mkstemp($dir, $prefix); + if (false !== $ffiPath) { + return $ffiPath; + } + + if (!\is_dir($dir) || !\is_writable($dir)) { + return false; + } + $path = \tempnam($dir, $prefix); + + return false === $path ? false : $path; + } + + private static function emitNotice(Frame $frame): void + { + if (null === $frame->vmContext) { + return; + } + $frame->vmContext->errors->triggerError( + self::NOTICE_MESSAGE, + ErrorReporter::E_NOTICE, + '' !== $frame->scriptPath ? $frame->scriptPath : null, + $frame->vmContext, + $frame, + $frame->callSiteLine + ); + } +} diff --git a/ext/standard/VmFsTempnamNative.php b/ext/standard/VmFsTempnamNative.php new file mode 100644 index 00000000000..e753dbbd32e --- /dev/null +++ b/ext/standard/VmFsTempnamNative.php @@ -0,0 +1,76 @@ += self::PATH_MAX) { + return false; + } + try { + $buf = $ffi->new('char['.\strlen($template).']', false); + for ($i = 0, $n = \strlen($template); $i < $n; ++$i) { + $buf[$i] = $template[$i]; + } + $fd = (int) $ffi->mkstemp(\FFI::addr($buf[0])); + if ($fd < 0) { + return false; + } + $ffi->close($fd); + + return \FFI::string(\FFI::addr($buf[0])); + } catch (\Throwable) { + return false; + } + } + + private static function ffi(): ?\FFI + { + if (null !== self::$ffi) { + return self::$ffi; + } + if (!\extension_loaded('ffi')) { + return null; + } + $v = getenv('PHP_COMPILER_DISABLE_FFI'); + if (false !== $v && '' !== $v && '0' !== $v && 'false' !== strtolower($v)) { + return null; + } + + $cdef = <<<'CDEF' +int mkstemp(char *template); +int close(int fd); +CDEF; + + foreach (['libc.so.6', 'libc.so'] as $lib) { + try { + self::$ffi = \FFI::cdef($cdef, $lib); + + return self::$ffi; + } catch (\Throwable) { + } + } + + return null; + } +} diff --git a/ext/standard/VmString.php b/ext/standard/VmString.php index 1960a358228..58a8f8b19c9 100644 --- a/ext/standard/VmString.php +++ b/ext/standard/VmString.php @@ -79,6 +79,33 @@ public static function coerceStringBuiltinArg( return self::coerceOperand($var); } + /** + * Coerce a path builtin operand (php-src Z_PARAM_PATH; rejects embedded NUL, #4401). + * + * @throws \ValueError when the path contains a null byte + * @throws \TypeError when the operand cannot be converted like Zend PHP 8.x + */ + public static function coercePathBuiltinArg( + Variable $var, + string $function, + int $argIndex = 0, + string $paramName = 'path' + ): string { + $str = self::coerceStringBuiltinArg($var, $function, $argIndex, $paramName); + if (str_contains($str, "\0")) { + throw new \ValueError( + sprintf( + '%s(): Argument #%d ($%s) must not contain any null bytes', + $function, + $argIndex + 1, + $paramName + ) + ); + } + + return $str; + } + /** * Coerce disk_*_space() directory operand; null means default path (php-src filestat.c, #4915). * diff --git a/ext/standard/tempnam.php b/ext/standard/tempnam.php index 7f7ae97c6ce..f19e18cb349 100644 --- a/ext/standard/tempnam.php +++ b/ext/standard/tempnam.php @@ -27,9 +27,9 @@ public function execute(Frame $frame): void if (null === $frame->returnVar) { return; } - $dir = VmString::coerceStringBuiltinArg($frame->calledArgs[0], 'tempnam', 0, 'directory'); - $prefix = VmString::coerceStringBuiltinArg($frame->calledArgs[1], 'tempnam', 1, 'prefix'); - $path = VmFs::tempnam($dir, $prefix); + $dir = VmString::coercePathBuiltinArg($frame->calledArgs[0], 'tempnam', 0, 'directory'); + $prefix = VmString::coercePathBuiltinArg($frame->calledArgs[1], 'tempnam', 1, 'prefix'); + $path = VmFsTempnam::invoke($dir, $prefix, $frame); if (false === $path) { $frame->returnVar->bool(false); } else { diff --git a/lib/JIT/Builtin/StringFsDirJit.php b/lib/JIT/Builtin/StringFsDirJit.php index 328111378dc..449dbd815fb 100644 --- a/lib/JIT/Builtin/StringFsDirJit.php +++ b/lib/JIT/Builtin/StringFsDirJit.php @@ -4,6 +4,7 @@ namespace PHPCompiler\JIT\Builtin; +use PHPCompiler\ext\standard\VmFsTempnam; use PHPCompiler\JIT\BasicBlockHelper; use PHPCompiler\JIT\Context; use PHPLLVM\Builder; @@ -787,13 +788,31 @@ private static function emitSysGetTempDir(Context $context, LlvmFunction $fn): v private static function emitTempnam(Context $context, LlvmFunction $fn): void { - $entry = $fn->appendBasicBlock('entry'); - $context->builder->positionAtEnd($entry); + TypeErrorRaise::ensureLinked($context); + StringTriggerError::ensureLinked($context); $i8 = $context->getTypeFromString('int8'); $i32 = $context->getTypeFromString('int32'); + $i64 = $context->getTypeFromString('int64'); $sizeT = $context->getTypeFromString('size_t'); + $i8p = $context->getTypeFromString('int8*'); $strPtr = $context->getTypeFromString('__string__*'); + $strMap = $context->structFieldMap['__string__']; + + foreach ([ + ['memchr', $i8p, [$i8p, $i32, $sizeT]], + ['strrchr', $i8p, [$i8p, $i32]], + ['strlen', $i64, [$i8p]], + ['memcpy', $i8p, [$i8p, $i8p, $sizeT]], + ['snprintf', $i32, [$i8p, $sizeT, $i8p]], + ['mkstemp', $i32, [$i8p]], + ['close', $i32, [$i32]], + ] as [$name, $ret, $params]) { + self::ensureExternal($context, $name, $context->context->functionType($ret, false, ...$params)); + } + + $entry = $fn->appendBasicBlock('entry'); + $context->builder->positionAtEnd($entry); $dirObj = $fn->getParam(0); $pfxObj = $fn->getParam(1); @@ -809,39 +828,222 @@ private static function emitTempnam(Context $context, LlvmFunction $fn): void $context->builder->positionAtEnd($body); $dir = self::stringData($context, $dirObj); $pfx = self::stringData($context, $pfxObj); + $dirLen = $context->builder->load($context->builder->structGep($dirObj, $strMap['length'])); + $pfxLen = $context->builder->load($context->builder->structGep($pfxObj, $strMap['length'])); + + self::emitTempnamRejectNullByte( + $context, + $fn, + $dir, + $dirLen, + 'tempnam(): Argument #1 ($directory) must not contain any null bytes' + ); + self::emitTempnamRejectNullByte( + $context, + $fn, + $pfx, + $pfxLen, + 'tempnam(): Argument #2 ($prefix) must not contain any null bytes' + ); + $dirEmpty = $context->builder->icmp(Builder::INT_EQ, $context->builder->load($dir), $i8->constInt(0, false)); $pfxEmpty = $context->builder->icmp(Builder::INT_EQ, $context->builder->load($pfx), $i8->constInt(0, false)); $empty = $context->builder->or($dirEmpty, $pfxEmpty); - $formatBlock = $fn->appendBasicBlock('tempnam_format'); - $context->builder->branchIf($empty, $fail, $formatBlock); + $normBlock = $fn->appendBasicBlock('tempnam_norm'); + $context->builder->branchIf($empty, $fail, $normBlock); + + $context->builder->positionAtEnd($normBlock); + $pfxBufSlot = BasicBlockHelper::entryAlloca($context, $i8->arrayType(64)); + $pfxBuf = $context->builder->pointerCast($pfxBufSlot, $i8p); + $startSlot = BasicBlockHelper::entryAlloca($context, $i8p); + $lastSepSlot = BasicBlockHelper::entryAlloca($context, $i8p); + $context->builder->store($i8p->constNull(), $lastSepSlot); + + $slash = $context->builder->call($context->lookupFunction('strrchr'), $pfx, $i32->constInt(ord('/'), false)); + $bslash = $context->builder->call($context->lookupFunction('strrchr'), $pfx, $i32->constInt(ord('\\'), false)); + $slashNull = $context->builder->icmp(Builder::INT_EQ, $slash, $i8p->constNull()); + $bslashNull = $context->builder->icmp(Builder::INT_EQ, $bslash, $i8p->constNull()); + + $afterSlash = $fn->appendBasicBlock('tempnam_after_slash'); + $slashSet = $fn->appendBasicBlock('tempnam_slash_set'); + $context->builder->branchIf($slashNull, $afterSlash, $slashSet); + $context->builder->positionAtEnd($slashSet); + $context->builder->store($slash, $lastSepSlot); + $context->builder->branch($afterSlash); + $context->builder->positionAtEnd($afterSlash); + + $afterBslash = $fn->appendBasicBlock('tempnam_after_bslash'); + $bslashCheck = $fn->appendBasicBlock('tempnam_bslash_check'); + $bslashSet = $fn->appendBasicBlock('tempnam_bslash_set'); + $context->builder->branchIf($bslashNull, $afterBslash, $bslashCheck); + $context->builder->positionAtEnd($bslashCheck); + $lastSep = $context->builder->load($lastSepSlot); + $lastSepNull = $context->builder->icmp(Builder::INT_EQ, $lastSep, $i8p->constNull()); + $bslashGt = $context->builder->icmp(Builder::INT_UGT, $bslash, $lastSep); + $context->builder->branchIf( + $context->builder->or($lastSepNull, $bslashGt), + $bslashSet, + $afterBslash + ); + $context->builder->positionAtEnd($bslashSet); + $context->builder->store($bslash, $lastSepSlot); + $context->builder->branch($afterBslash); + $context->builder->positionAtEnd($afterBslash); + + $copyBlock = $fn->appendBasicBlock('tempnam_copy_prefix'); + $usePfxStart = $fn->appendBasicBlock('tempnam_use_pfx_start'); + $useLastStart = $fn->appendBasicBlock('tempnam_use_last_start'); + $lastSep = $context->builder->load($lastSepSlot); + $lastSepNull = $context->builder->icmp(Builder::INT_EQ, $lastSep, $i8p->constNull()); + $context->builder->branchIf($lastSepNull, $usePfxStart, $useLastStart); + $context->builder->positionAtEnd($usePfxStart); + $context->builder->store($pfx, $startSlot); + $context->builder->branch($copyBlock); + $context->builder->positionAtEnd($useLastStart); + $context->builder->store($context->builder->gep($lastSep, $i64->constInt(1, false)), $startSlot); + $context->builder->branch($copyBlock); - $context->builder->positionAtEnd($formatBlock); + $context->builder->positionAtEnd($copyBlock); + $start = $context->builder->load($startSlot); + $baseLen = $context->builder->call($context->lookupFunction('strlen'), $start); + $maxCopy = $sizeT->constInt(63, false); + $copyLen = $context->builder->select( + $context->builder->icmp(Builder::INT_ULT, $baseLen, $maxCopy), + $context->builder->intCast($baseLen, $sizeT), + $maxCopy + ); + $context->builder->call($context->lookupFunction('memcpy'), $pfxBuf, $start, $copyLen); + $context->builder->store( + $i8->constInt(0, false), + $context->builder->gep($pfxBuf, $context->builder->intCast($copyLen, $i64)) + ); + + $tryPrimary = $fn->appendBasicBlock('tempnam_try_primary'); + $context->builder->branch($tryPrimary); + $context->builder->positionAtEnd($tryPrimary); $tplSlot = BasicBlockHelper::entryAlloca($context, $i8->arrayType(self::PATH_MAX)); - $tpl = $context->builder->pointerCast($tplSlot, $context->getTypeFromString('int8*')); + $tpl = $context->builder->pointerCast($tplSlot, $i8p); + $primaryOk = self::emitTempnamMkstempAttempt($context, $fn, $dir, $pfxBuf, $tpl, 'tempnam_primary'); + $retPrimary = $fn->appendBasicBlock('tempnam_ret_primary'); + $fallback = $fn->appendBasicBlock('tempnam_fallback'); + $context->builder->branchIf($primaryOk, $retPrimary, $fallback); + + $context->builder->positionAtEnd($retPrimary); + $context->builder->returnValue(self::cstrToString($context, $tpl)); + + $context->builder->positionAtEnd($fallback); + self::emitTempnamNotice($context); + $fallbackDir = $context->builder->call($context->lookupFunction('__compiler_sys_get_temp_dir')); + $fallbackNull = $context->builder->icmp(Builder::INT_EQ, $fallbackDir, $nullStr); + $tryFallback = $fn->appendBasicBlock('tempnam_try_fallback'); + $context->builder->branchIf($fallbackNull, $fail, $tryFallback); + $context->builder->positionAtEnd($tryFallback); + $fallbackData = self::stringData($context, $fallbackDir); + $fallbackOk = self::emitTempnamMkstempAttempt($context, $fn, $fallbackData, $pfxBuf, $tpl, 'tempnam_fb'); + $retFallback = $fn->appendBasicBlock('tempnam_ret_fallback'); + $context->builder->branchIf($fallbackOk, $retFallback, $fail); + $context->builder->positionAtEnd($retFallback); + $context->builder->returnValue(self::cstrToString($context, $tpl)); + + $context->builder->positionAtEnd($fail); + $context->builder->returnValue($nullStr); + } + + private static function emitTempnamRejectNullByte( + Context $context, + LlvmFunction $fn, + Value $data, + Value $len, + string $message + ): void { + $i8p = $context->getTypeFromString('int8*'); + $i32 = $context->getTypeFromString('int32'); + $sizeT = $context->getTypeFromString('size_t'); + $found = $context->builder->call( + $context->lookupFunction('memchr'), + $data, + $i32->constInt(0, false), + $context->builder->intCast($len, $sizeT) + ); + $hasNull = $context->builder->icmp(Builder::INT_NE, $found, $i8p->constNull()); + static $rejectSeq = 0; + $tag = 'tempnam_nul_'.(string) (++$rejectSeq); + $ok = $fn->appendBasicBlock($tag.'_ok'); + $bad = $fn->appendBasicBlock($tag.'_bad'); + $context->builder->branchIf($hasNull, $bad, $ok); + $context->builder->positionAtEnd($bad); + TypeErrorRaise::emitValueError($context, $message); + $context->builder->call($context->lookupFunction('abort')); + $context->builder->positionAtEnd($ok); + } + + private static function emitTempnamNotice(Context $context): void + { + $i8p = $context->getTypeFromString('int8*'); + $i32 = $context->getTypeFromString('int32'); + $i64 = $context->getTypeFromString('int64'); + $message = VmFsTempnam::NOTICE_MESSAGE; + $msgPtr = $context->builder->pointerCast($context->constantFromString($message), $i8p); + $msgLen = $context->builder->call($context->lookupFunction('strlen'), $msgPtr); + $emptyFile = $context->builder->pointerCast($context->constantFromString(''), $i8p); + $context->builder->call( + $context->lookupFunction('__compiler_trigger_error'), + $msgPtr, + $msgLen, + $i32->constInt(8, false), + $emptyFile, + $i32->constInt(0, false) + ); + } + + private static function emitTempnamMkstempAttempt( + Context $context, + LlvmFunction $fn, + Value $dir, + Value $pfxBuf, + Value $tpl, + string $tag + ): Value { + $i32 = $context->getTypeFromString('int32'); + $sizeT = $context->getTypeFromString('size_t'); + $format = $fn->appendBasicBlock($tag.'_format'); + $mkstempBb = $fn->appendBasicBlock($tag.'_mkstemp'); + $closeBb = $fn->appendBasicBlock($tag.'_close'); + $failBb = $fn->appendBasicBlock($tag.'_fail'); + $doneBb = $fn->appendBasicBlock($tag.'_done'); + + $context->builder->branch($format); + $context->builder->positionAtEnd($format); $n = $context->builder->call( $context->lookupFunction('snprintf'), $tpl, $sizeT->constInt(self::PATH_MAX, false), self::literalCstr($context, '%s/%sXXXXXX'), $dir, - $pfx + $pfxBuf ); $tooLong = $context->builder->icmp(Builder::INT_SGE, $n, $i32->constInt(self::PATH_MAX, false)); - $mkstemp = $fn->appendBasicBlock('tempnam_mkstemp'); - $context->builder->branchIf($tooLong, $fail, $mkstemp); + $context->builder->branchIf($tooLong, $failBb, $mkstempBb); - $context->builder->positionAtEnd($mkstemp); + $context->builder->positionAtEnd($mkstempBb); $fd = $context->builder->call($context->lookupFunction('mkstemp'), $tpl); $fdBad = $context->builder->icmp(Builder::INT_SLT, $fd, $i32->constInt(0, true)); - $close = $fn->appendBasicBlock('tempnam_close'); - $context->builder->branchIf($fdBad, $fail, $close); + $context->builder->branchIf($fdBad, $failBb, $closeBb); - $context->builder->positionAtEnd($close); + $context->builder->positionAtEnd($closeBb); $context->builder->call($context->lookupFunction('close'), $fd); - $context->builder->returnValue(self::cstrToString($context, $tpl)); + $context->builder->branch($doneBb); - $context->builder->positionAtEnd($fail); - $context->builder->returnValue($nullStr); + $context->builder->positionAtEnd($failBb); + $context->builder->branch($doneBb); + + $context->builder->positionAtEnd($doneBb); + $i1 = $context->getTypeFromString('int1'); + $okPhi = $context->builder->phi($i1, $tag.'_ok'); + $okPhi->addIncoming($i1->constInt(0, false), $failBb); + $okPhi->addIncoming($i1->constInt(1, false), $closeBb); + + return $okPhi; } private static function resolveIdFromValue(Context $context, Value $value, bool $group): Value diff --git a/test/compliance/cases/stdlib/tempnam_invalid_dir_fallback.phpt b/test/compliance/cases/stdlib/tempnam_invalid_dir_fallback.phpt new file mode 100644 index 00000000000..59a7acb99f3 --- /dev/null +++ b/test/compliance/cases/stdlib/tempnam_invalid_dir_fallback.phpt @@ -0,0 +1,10 @@ +--TEST-- +stdlib tempnam() invalid directory falls back to sys temp dir with Notice (#4401) +--FILE-- +getMessage(), "\n"; +} +--EXPECT-- +tempnam(): Argument #1 ($directory) must not contain any null bytes diff --git a/test/compliance/cases/stdlib/tempnam_prefix_length.phpt b/test/compliance/cases/stdlib/tempnam_prefix_length.phpt new file mode 100644 index 00000000000..5a9ad56d03d --- /dev/null +++ b/test/compliance/cases/stdlib/tempnam_prefix_length.phpt @@ -0,0 +1,11 @@ +--TEST-- +stdlib tempnam() prefix truncation to 63 chars (php-src file.c, #4401) +--FILE-- +getMessage(), "\n"; +} + +@rmdir($dir);