Skip to content
Closed
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
87 changes: 53 additions & 34 deletions ext/mbstring/JitMbScrub.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
use PHPLLVM\Value;

/**
* LLVM JIT/AOT for mb_scrub() (php-src ext/mbstring/mbstring.c; #6050, #34338).
* LLVM JIT/AOT for mb_scrub() (php-src ext/mbstring/mbstring.c; #6050, #34338, #35161).
*
* Compile-time fold for string literals; runtime string + encoding literal via NestedJIT
* {@see MbScrubJitHelper} (peer {@see JitMbCase} / {@see JitMbConvertEncoding}).
* Compile-time fold for string literals; runtime string via NestedJIT
* {@see MbScrubJitHelper}. Runtime encoding via NestedJIT assertEncodingArgv
* (#35161 leftover of #34338 / peer #35155 / #35151).
*/
final class JitMbScrub
{
Expand All @@ -37,15 +38,13 @@ public static function invoke(Context $context, array $args): Value
return $folded;
}

$encoding = self::runtimeEncodingLiteral($args, $argc);
if (null === $encoding) {
throw new \LogicException(
'mb_scrub() encoding must be a string literal in this compiler build'
);
}
if (null === self::canonicalEncoding($encoding)) {
return self::emitEncodingValueError($context, $encoding);
// Link NestedJIT helpers before lowering args — NestedJIT can invalidate prior IR (#34270 / #35161).
$savedInsert = BasicBlockHelper::tryGetInsertBlock($context);
MbScrubRuntime::ensureLinked($context);
if (null !== $savedInsert) {
BasicBlockHelper::restoreInsertBlock($context, $savedInsert);
}
BasicBlockHelper::ensureOpenInsertBlock($context, 'mb_scrub_runtime');

// Soft-null DEP+coerce on 8.4 (php-src mbstring.c; #21516).
$str = JitStringBuiltinArg::lowerTrimFamilyString(
Expand All @@ -55,16 +54,16 @@ public static function invoke(Context $context, array $args): Value
0,
'string'
);

// NestedJIT helper compile can clear insert; restore before call (#34270 peer).
$savedInsert = BasicBlockHelper::tryGetInsertBlock($context);
MbScrubRuntime::ensureLinked($context);
if (null !== $savedInsert) {
BasicBlockHelper::restoreInsertBlock($context, $savedInsert);
[$encPtr, $needsAssert] = self::encodingPtr($context, $args, $argc);
if ($needsAssert) {
$fnName = $context->builder->load($context->constantStringFromString('mb_scrub'));
$context->builder->call(
MbScrubRuntime::assertEncodingHelper($context),
$encPtr,
$fnName
);
}
BasicBlockHelper::ensureOpenInsertBlock($context, 'mb_scrub_runtime');

$encPtr = $context->builder->load($context->constantStringFromString($encoding));
$resultStr = $context->builder->call(
MbScrubRuntime::scrubHelper($context),
$str,
Expand Down Expand Up @@ -106,36 +105,56 @@ public static function tryCompileTimeFold(Context $context, array $args): ?Value
}

/**
* @param JITVariable[] $args
* Literal UTF-8/ASCII/8BIT → constant string (no assert); otherwise NestedJIT encoding + assert (#35161).
*
* @param list<JITVariable> $args
* @return array{0: Value, 1: bool} encoding ptr, needsAssert
*/
private static function compileTimeEncoding(array $args, int $index): ?string
private static function encodingPtr(Context $context, array $args, int $argc): array
{
if (!isset($args[$index])) {
return 'UTF-8';
}
if (JITVariable::TYPE_NULL === $args[$index]->type || ($args[$index]->isNullConstant ?? false)) {
return 'UTF-8';
if ($argc < 2 || JITVariable::TYPE_NULL === $args[1]->type || ($args[1]->isNullConstant ?? false)) {
return [$context->builder->load($context->constantStringFromString('UTF-8')), false];
}
if (JITVariable::TYPE_STRING !== $args[$index]->type) {
return null;

$encodingLit = JitStringArg::compileTimeLiteral($args[1]);
if (null !== $encodingLit) {
$canonical = self::canonicalEncoding($encodingLit);
if (null !== $canonical) {
return [$context->builder->load($context->constantStringFromString($canonical)), false];
}

// Invalid / unsupported literal — NestedJIT assert throws catchable ValueError (#35161).
return [$context->builder->load($context->constantStringFromString($encodingLit)), true];
}

return $args[$index]->compileTimeString ?? null;
return [
JitStringBuiltinArg::lower(
$context,
$args[1],
'mb_scrub',
1,
'encoding'
),
true,
];
}

/**
* @param list<JITVariable> $args
* @param JITVariable[] $args
*/
private static function runtimeEncodingLiteral(array $args, int $argc): ?string
private static function compileTimeEncoding(array $args, int $index): ?string
{
if ($argc < 2) {
if (!isset($args[$index])) {
return 'UTF-8';
}
if (JITVariable::TYPE_NULL === $args[1]->type || ($args[1]->isNullConstant ?? false)) {
if (JITVariable::TYPE_NULL === $args[$index]->type || ($args[$index]->isNullConstant ?? false)) {
return 'UTF-8';
}
if (JITVariable::TYPE_STRING !== $args[$index]->type) {
return null;
}

return JitStringArg::compileTimeLiteral($args[1]);
return $args[$index]->compileTimeString ?? null;
}

private static function canonicalEncoding(string $encoding): ?string
Expand Down
40 changes: 36 additions & 4 deletions ext/mbstring/MbScrubJitHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,50 @@
namespace PHPCompiler\ext\mbstring;

/**
* mb_scrub() NestedJIT runtime (#34338 leftover of #6050).
* mb_scrub() NestedJIT runtime (#34338 leftover of #6050 / #35161).
*
* Leaf UTF-8 / ASCII / 8BIT scrub with '?' substitution (Zend default
* mb_substitute_character). Encoding is always a compile-time-validated
* literal from {@see JitMbScrub} — no ValueError here (NestedJIT throw
* paths pollute the user module).
* mb_substitute_character). Runtime encoding via {@see assertEncodingArgv}
* (#35161 leftover of #34338 / peer #35155).
*
* php-src: ext/mbstring/mbstring.c — PHP_FUNCTION(mb_scrub)
*/
final class MbScrubJitHelper
{
/**
* Int-returning encoding check — NestedJIT ValueError from string-returning helpers
* SIGSEGVs under thin AOT; int helpers match {@see MbSubstrCountJitHelper::assertEncodingArgv} (#35161).
*
* Argument #2 ($encoding) for mb_scrub.
*/
public static function assertEncodingArgv(string $encoding, string $function): int
{
$ok = 0;
if ('UTF-8' === $encoding || 'utf-8' === $encoding || 'UTF8' === $encoding || 'utf8' === $encoding) {
$ok = 1;
}
if (
'ASCII' === $encoding || 'ascii' === $encoding
|| 'US-ASCII' === $encoding || 'us-ascii' === $encoding
) {
$ok = 1;
}
if ('8BIT' === $encoding || '8bit' === $encoding || 'BINARY' === $encoding || 'binary' === $encoding) {
$ok = 1;
}
if (0 === $ok) {
// Concat (not sprintf) — NestedJIT sprintf+throw breaks module verify (#34625).
throw new \ValueError(
$function.'(): Argument #2 ($encoding) must be a valid encoding, "'.$encoding.'" given'
);
}

return 1;
}

public static function scrubArgv(string $value, string $encoding): string
{
// Encoding must already be validated via {@see assertEncodingArgv} for runtime paths (#35161).
$canonical = self::canonical($encoding);
if ('8BIT' === $canonical) {
return $value;
Expand Down
14 changes: 13 additions & 1 deletion lib/JIT/Builtin/MbScrubRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
use PHPLLVM\Value\Function_ as LlvmFunction;

/**
* JIT/AOT link hook for mb_scrub() — MbScrubJitHelper (#34338 / #6050).
* JIT/AOT link hook for mb_scrub() — MbScrubJitHelper (#34338 / #6050 / #35161).
*
* Runtime encoding assert: {@see MbScrubJitHelper::assertEncodingArgv} (#35161).
*
* php-src: ext/mbstring/mbstring.c — PHP_FUNCTION(mb_scrub)
*/
Expand All @@ -19,9 +21,12 @@ final class MbScrubRuntime

private const SCRUB_LOGICAL = 'PHPCompiler\\ext\\mbstring\\MbScrubJitHelper::scrubArgv';

private const ASSERT_ENCODING_LOGICAL = 'PHPCompiler\\ext\\mbstring\\MbScrubJitHelper::assertEncodingArgv';

/** @var list<string> */
private const COMPILED_HELPERS = [
self::SCRUB_LOGICAL,
self::ASSERT_ENCODING_LOGICAL,
];

public static function ensureLinked(Context $context): void
Expand All @@ -36,6 +41,13 @@ public static function scrubHelper(Context $context): LlvmFunction
return JitVmHelperLink::lookupCompiled($context, self::SCRUB_LOGICAL, 'mb_scrub');
}

public static function assertEncodingHelper(Context $context): LlvmFunction
{
self::ensureJitHelperCompiled($context);

return JitVmHelperLink::lookupCompiled($context, self::ASSERT_ENCODING_LOGICAL, 'mb_scrub_encoding');
}

private static function ensureJitHelperCompiled(Context $context): void
{
JitVmHelperLink::ensureCompiled(
Expand Down
21 changes: 21 additions & 0 deletions test/repro/mb_scrub_runtime_encoding_aot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

/**
* #35161 — mb_scrub() with runtime encoding under thin AOT.
* php-src: ext/mbstring/mbstring.c PHP_FUNCTION(mb_scrub)
*/
$enc = 'UTF-8';
echo bin2hex(mb_scrub("a\xC0b", $enc)), "\n";
$ascii = 'ASCII';
echo bin2hex(mb_scrub("a\xC0b", $ascii)), "\n";
$bit = '8bit';
echo bin2hex(mb_scrub("a\xC0b", $bit)), "\n";
try {
$bad = 'nope';
echo mb_scrub('x', $bad);
echo "no error\n";
} catch (ValueError $e) {
echo 'bad_enc=', $e->getMessage(), "\n";
}
19 changes: 18 additions & 1 deletion test/unit/MbScrubRuntimeAotTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use PHPUnit\Framework\TestCase;

/**
* AOT: mb_scrub() via MbScrubJitHelper (#34338 leftover of #6050).
* AOT: mb_scrub() via MbScrubJitHelper (#34338 leftover of #6050 / #35161).
*
* @see php-src ext/mbstring/mbstring.c PHP_FUNCTION(mb_scrub)
*
Expand All @@ -24,14 +24,31 @@ public function testAotRuntimeMatchMatchesZend(): void
$this->assertAotMatchesZend(__DIR__.'/../repro/aot_mb_scrub_runtime.php');
}

public function testAotRuntimeEncodingMatchesZend(): void
{
if (!LlvmToolchain::hasLibrary(dirname(__DIR__, 2))) {
$this->markTestSkipped('LLVM 9 toolchain not available');
}
$this->assertAotMatchesZend(__DIR__.'/../repro/mb_scrub_runtime_encoding_aot.php');
}

public function testHelperAndLoweringPresent(): void
{
$root = dirname(__DIR__, 2);
$helper = (string) file_get_contents($root.'/ext/mbstring/MbScrubJitHelper.php');
$this->assertStringContainsString('function scrubArgv', $helper);
$this->assertStringContainsString('function assertEncodingArgv', $helper);
$this->assertStringContainsString('Argument #2', $helper);
$runtime = (string) file_get_contents($root.'/lib/JIT/Builtin/MbScrubRuntime.php');
$this->assertStringContainsString('scrubHelper', $runtime);
$this->assertStringContainsString('assertEncodingHelper', $runtime);
$this->assertStringContainsString('MbScrubJitHelper::scrubArgv', $runtime);
$jit = (string) file_get_contents($root.'/ext/mbstring/JitMbScrub.php');
$this->assertStringContainsString('encodingPtr', $jit);
$this->assertStringNotContainsString(
'encoding must be a string literal in this compiler build',
$jit
);
$src = (string) file_get_contents($root.'/ext/mbstring/mb_scrub.php');
$this->assertStringContainsString('JitMbScrub::invoke', $src);
$this->assertStringNotContainsString(
Expand Down