Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ext/standard/JitTempnam.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
14 changes: 10 additions & 4 deletions ext/standard/VmFs.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
77 changes: 77 additions & 0 deletions ext/standard/VmFsTempnam.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\standard;

use PHPCompiler\Frame;
use PHPCompiler\VM\ErrorReporter;

/**
* tempnam() path creation (php-src ext/standard/file.c, main/php_open_temporary_file.c, #4401).
*/
final class VmFsTempnam
{
private const PREFIX_MAX = 64;

public const NOTICE_MESSAGE = "tempnam(): file created in the system's temporary directory";

public static function normalizePrefix(string $prefix): string
{
$base = VmString::basename($prefix, '');
if (\strlen($base) >= 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
);
}
}
76 changes: 76 additions & 0 deletions ext/standard/VmFsTempnamNative.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\standard;

/**
* libc mkstemp(3) for tempnam() without host PHP delegation (#4401).
*/
final class VmFsTempnamNative
{
private const PATH_MAX = 4096;

private static ?\FFI $ffi = null;

public static function mkstemp(string $dir, string $prefix): string|false
{
if (str_contains($dir, "\0") || str_contains($prefix, "\0")) {
return false;
}
$ffi = self::ffi();
if (null === $ffi) {
return false;
}
$sep = ('/' === $dir[\strlen($dir) - 1] || '\\' === $dir[\strlen($dir) - 1]) ? '' : '/';
$template = $dir.$sep.$prefix.'XXXXXX';
if (\strlen($template) >= 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;
}
}
27 changes: 27 additions & 0 deletions ext/standard/VmString.php
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
Expand Down
6 changes: 3 additions & 3 deletions ext/standard/tempnam.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading