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
7 changes: 7 additions & 0 deletions lib/JIT.php
Original file line number Diff line number Diff line change
Expand Up @@ -15827,6 +15827,13 @@ private function instanceMethodUsesThis(Block $block): bool
if (null !== $block->func->class) {
return true;
}
// Script top-level never receives $this. Without this guard the #16075 scope
// fallback below hands `{main}` an `%__object__*` parameter whenever a class was
// compiled first, and standalone main's `call void @internal_N()` then fails
// verification with an argument-count mismatch (#22638).
if ('{main}' === $block->func->name) {
return false;
}
// Nested file JIT: func->class may be unset while scope carries the declaring class (#16075).
if ('' !== $this->context->scope->className) {
return true;
Expand Down
12 changes: 9 additions & 3 deletions lib/JIT/Builtin/ParamSensitiveLookupRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ private static function implementFunctionBridge(Context $context, array $functio
$i1 = $context->getTypeFromString('int1');
$i8p = $context->getTypeFromString('int8*');
$i64 = $context->getTypeFromString('int64');
// strcmp is declared i32 in LibcExtern; an i64 zero makes the icmp operands
// disagree and fails module verification (#22638).
$i32Zero = $context->getTypeFromString('int32')->constInt(0, false);
$ft = $context->context->functionType($i1, false, $i8p, $i64);
$fn = $context->module->addFunction($abiName, $ft);
$entry = $fn->appendBasicBlock('param_func_is_sensitive_entry');
Expand Down Expand Up @@ -69,7 +72,7 @@ private static function implementFunctionBridge(Context $context, array $functio
$funcCstr,
$context->builder->pointerCast($expected, $i8p)
);
$nameOk = $context->builder->icmp(Builder::INT_EQ, $nameEq, $i64->constInt(0, false));
$nameOk = $context->builder->icmp(Builder::INT_EQ, $nameEq, $i32Zero);
$idxOk = $context->builder->icmp(Builder::INT_EQ, $idx, $i64->constInt((int) $paramIndex, false));
$both = $context->builder->and($nameOk, $idxOk);
$context->builder->branchIf($both, $match, $merge);
Expand Down Expand Up @@ -105,6 +108,9 @@ private static function implementMethodBridge(Context $context, array $methodPar
$i1 = $context->getTypeFromString('int1');
$i8p = $context->getTypeFromString('int8*');
$i64 = $context->getTypeFromString('int64');
// strcmp is declared i32 in LibcExtern; an i64 zero makes the icmp operands
// disagree and fails module verification (#22638).
$i32Zero = $context->getTypeFromString('int32')->constInt(0, false);
$ft = $context->context->functionType($i1, false, $i8p, $i8p, $i64);
$fn = $context->module->addFunction($abiName, $ft);
$entry = $fn->appendBasicBlock('param_method_is_sensitive_entry');
Expand Down Expand Up @@ -144,15 +150,15 @@ private static function implementMethodBridge(Context $context, array $methodPar
$classCstr,
$context->builder->pointerCast($classExpected, $i8p)
);
$classOk = $context->builder->icmp(Builder::INT_EQ, $classEq, $i64->constInt(0, false));
$classOk = $context->builder->icmp(Builder::INT_EQ, $classEq, $i32Zero);

$methodExpected = $context->constantFromString(strtolower($methodLc));
$methodEq = $context->builder->call(
$context->lookupFunction('strcmp'),
$methodCstr,
$context->builder->pointerCast($methodExpected, $i8p)
);
$methodOk = $context->builder->icmp(Builder::INT_EQ, $methodEq, $i64->constInt(0, false));
$methodOk = $context->builder->icmp(Builder::INT_EQ, $methodEq, $i32Zero);

$posOk = $context->builder->icmp(Builder::INT_EQ, $pos, $i64->constInt((int) $position, false));
$all = $context->builder->and($classOk, $methodOk);
Expand Down
9 changes: 7 additions & 2 deletions lib/JIT/Builtin/ReflectionFunctionVariadicLookupRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public static function implement(Context $context, string $variadicJson): void
$names = self::decodeNames($variadicJson);
$i1 = $context->getTypeFromString('int1');
$i8p = $context->getTypeFromString('int8*');
$i64 = $context->getTypeFromString('int64');
$ft = $context->context->functionType($i1, false, $i8p);
$fn = $context->module->addFunction($abiName, $ft);
$entry = $fn->appendBasicBlock('refl_func_is_variadic_entry');
Expand Down Expand Up @@ -60,7 +59,13 @@ public static function implement(Context $context, string $variadicJson): void
$funcCstr,
$context->builder->pointerCast($expected, $i8p)
);
$nameOk = $context->builder->icmp(Builder::INT_EQ, $nameEq, $i64->constInt(0, false));
// strcmp is declared i32 in LibcExtern; an i64 zero here makes the icmp
// operands disagree and fails module verification (#22638).
$nameOk = $context->builder->icmp(
Builder::INT_EQ,
$nameEq,
$context->getTypeFromString('int32')->constInt(0, false)
);
$context->builder->branchIf($nameOk, $match, $merge);

$context->builder->positionAtEnd($match);
Expand Down
55 changes: 51 additions & 4 deletions lib/JIT/Builtin/StringOffsetRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use PHPCompiler\JIT\BasicBlockHelper;
use PHPCompiler\JIT\Context;
use PHPCompiler\JIT\JitLongArg;
use PHPCompiler\JIT\JitValueBox;
use PHPCompiler\JIT\JitVmHelperLink;
use PHPCompiler\JIT\NestedJitCompileScope;
use PHPCompiler\JIT\Variable as JitVariable;
Expand Down Expand Up @@ -115,7 +117,10 @@ public static function dimFetch(Context $context, Value $str, JitVariable $dim):
$len = $context->builder->load(
$context->builder->structGep($str, $map['length'])
);
$index = $context->helper->loadValue($dim);
// `$s[$i]` accepts int, bool, null, float and numeric-string offsets; JitLongArg
// applies those coercions and always hands back an i64. A raw loadValue() here
// returned the `%__value__` struct for a box-backed offset (#22638).
$index = JitLongArg::lower($context, $dim, 'string offset');
$offset = self::normalizeOffset($context, $index, $len);

return $context->builder->gep($chars, $offset);
Expand All @@ -125,17 +130,59 @@ public static function normalizeOffset(Context $context, Value $index, Value $le
{
self::ensureLinked($context);
$fn = $context->lookupFunction(self::ABI_NORMALIZE);
$i64 = $context->getTypeFromString('int64');
$sizeT = $context->getTypeFromString('size_t');
$normalized = $context->builder->call(
$fn,
$context->builder->zext($index, $i64),
$context->builder->zext($len, $i64)
self::offsetToI64($context, $index),
self::offsetToI64($context, $len)
);

return $context->builder->truncOrBitCast($normalized, $sizeT);
}

/**
* Coerce an offset/length operand to the i64 {@see ABI_NORMALIZE} declares.
*
* Callers hand over whatever `loadValue()` produced: a box-backed offset is a
* `%__value__` struct, a native length is already i64. The blind `zext` this
* replaces emitted `zext %__value__ ... to i64`, which fails module verification
* — and since every JIT context builds the htmlspecialchars helper, and that
* helper indexes a string, the one bad instruction poisoned every helper unit
* and every user-script AOT module (#22638).
*/
private static function offsetToI64(Context $context, Value $value): Value
{
$i64 = $context->getTypeFromString('int64');
$name = $context->getStringFromType($value->typeOf());
if (1 === preg_match('/^int(\d+)$/', $name, $match)) {
$width = (int) $match[1];
if (64 === $width) {
return $value;
}

return $width < 64
? $context->builder->zext($value, $i64)
: $context->builder->truncOrBitCast($value, $i64);
}
if ('__value__' === $name || '__value__*' === $name) {
return $context->builder->call(
$context->lookupFunction('__value__readLong'),
'__value__*' === $name ? $value : self::spillValueBox($context, $value)
);
}

throw new \LogicException("string offset operand must be an integer, got {$name}");
}

/** Address a `%__value__` operand that arrived by value rather than by pointer. */
private static function spillValueBox(Context $context, Value $value): Value
{
$slot = $context->builder->alloca($value->typeOf(), 1, 'string_offset_box');
$context->builder->store($value, $slot);

return JitValueBox::pointer($context, $slot);
}

public static function dimAssign(Context $context, Value $charPtr, JitVariable $value): void
{
if (self::assignRhsIsEmptyAtCompileTime($value)) {
Expand Down
25 changes: 22 additions & 3 deletions lib/JIT/JitNestedHelperCoerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,25 @@ public static function isValueBox(Context $context, Value $value): bool
return self::isValueBoxType($context, $value->typeOf());
}

/**
* Materialize a freshly boxed slot for the callee's parameter type.
*
* {@see isValueBoxType} accepts both `__value__*` and `__value__`, but a box
* slot is addressed by pointer. A helper whose parameter is `__value__` by
* value needs the slot loaded — passing the pointer instead emitted
* `call ...(%__value__* %slot)` against a by-value parameter and failed
* module verification for every helper unit (#22638).
*/
private static function valueBoxAs(Context $context, Value $slot, Type $wantTy): Value
{
$ptr = JitValueBox::pointer($context, $slot);
if ('__value__' === $context->getStringFromType($wantTy)) {
return $context->builder->load($ptr);
}

return $ptr;
}

public static function isHelperResultNull(Context $context, Value $raw): Value
{
if (self::isValueBox($context, $raw)) {
Expand Down Expand Up @@ -173,7 +192,7 @@ public static function coerceArgForHelper(Context $context, Value $arg, Type $wa
$arg
);

return JitValueBox::pointer($context, $slot);
return self::valueBoxAs($context, $slot, $wantTy);
}
if ('__value__*' === $haveStr && '__value__' === $wantStr) {
return $context->builder->load($arg);
Expand All @@ -186,7 +205,7 @@ public static function coerceArgForHelper(Context $context, Value $arg, Type $wa
$arg
);

return JitValueBox::pointer($context, $slot);
return self::valueBoxAs($context, $slot, $wantTy);
}
if (('double' === $wantStr || 'float' === $wantStr) && self::isValueBoxType($context, $haveTy)) {
$extracted = self::extractDoubleFromHelperResult($context, $arg);
Expand All @@ -205,7 +224,7 @@ public static function coerceArgForHelper(Context $context, Value $arg, Type $wa
$asDouble
);

return JitValueBox::pointer($context, $slot);
return self::valueBoxAs($context, $slot, $wantTy);
}
if (Type::KIND_INTEGER === $wantTy->getKind() && Type::KIND_INTEGER === $haveTy->getKind()) {
if (('int8' === $haveStr || 'i8' === $haveStr) && ('int32' === $wantStr || 'int64' === $wantStr || 'long long' === $wantStr)) {
Expand Down
24 changes: 24 additions & 0 deletions patches/php-llvm-icmp-assert.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--- vendor/ircmaxell/php-llvm/lib/LLVMAbstract/Builder.php
+++ vendor/ircmaxell/php-llvm/lib/LLVMAbstract/Builder.php
@@ -423,6 +423,21 @@
}

public function iCmp(int $op, CoreValue $left, CoreValue $right): CoreValue {
+ if ('1' === \getenv('PHP_COMPILER_LLVM_ASSERT')) {
+ $lt = $this->llvm->lib->LLVMTypeOf($left->value);
+ $rt = $this->llvm->lib->LLVMTypeOf($right->value);
+ $lk = $this->llvm->lib->LLVMGetTypeKind($lt);
+ $rk = $this->llvm->lib->LLVMGetTypeKind($rt);
+ if ($lk === $rk && \llvm\llvm::LLVMIntegerTypeKind === $lk) {
+ $lw = $this->llvm->lib->LLVMGetIntTypeWidth($lt);
+ $rw = $this->llvm->lib->LLVMGetIntTypeWidth($rt);
+ if ($lw !== $rw) {
+ throw new \RuntimeException("iCmp: operand width mismatch (i{$lw} vs i{$rw})");
+ }
+ } elseif ($lk !== $rk) {
+ throw new \RuntimeException("iCmp: operand kind mismatch ({$lk} vs {$rk})");
+ }
+ }
switch ($op) {
case self::INT_EQ:
$predicate = lib::LLVMIntEQ;
4 changes: 4 additions & 0 deletions script/apply-patches.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ patch_already_applied() {
php-llvm-structgep-assert.patch)
grep -q 'PHP_COMPILER_LLVM_ASSERT' "$ROOT/vendor/ircmaxell/php-llvm/lib/LLVMAbstract/Builder.php" 2>/dev/null
;;
php-llvm-icmp-assert.patch)
grep -q 'iCmp: operand width mismatch' "$ROOT/vendor/ircmaxell/php-llvm/lib/LLVMAbstract/Builder.php" 2>/dev/null
;;
php-llvm-builder-dispose-idempotent.patch)
grep -q 'private bool \$disposed = false' "$ROOT/vendor/ircmaxell/php-llvm/lib/LLVMAbstract/Builder.php" 2>/dev/null \
|| grep -q 'private bool $disposed = false' "$ROOT/vendor/ircmaxell/php-llvm/lib/LLVMAbstract/Builder.php" 2>/dev/null
Expand Down Expand Up @@ -6274,6 +6277,7 @@ apply_patch "$PATCH_DIR/php-llvm-llvmabstract-value-addincoming.patch"
apply_patch "$PATCH_DIR/php-llvm-builder-and-or.patch"
apply_patch "$PATCH_DIR/php-llvm-builder-xor.patch"
apply_patch "$PATCH_DIR/php-llvm-structgep-assert.patch"
apply_patch "$PATCH_DIR/php-llvm-icmp-assert.patch"
apply_patch "$PATCH_DIR/php-llvm-pass-registry-interface.patch"
apply_patch "$PATCH_DIR/php-llvm-pass-manager-builder-semicolon.patch"
apply_patch "$PATCH_DIR/php-llvm-pass-manager-builder-typed-prop.patch"
Expand Down
Loading