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
7 changes: 5 additions & 2 deletions lib/JIT/Builtin/StringHashCryptoJit.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* LLVM lowering for hash() / hash_hmac() / hash_pbkdf2() / hash_equals() / hash_hmac_algos().
*
* Compiles {@see hash_crypto_jit_runtime.c} to bitcode for MCJIT and AOT module link (#7060).
* Removed from {@see lib/AOT/Linker.php} — digest bodies live in the JIT link step, not AOT .o floor.
* hash_equals / hash_hmac_algos lowered in PHP LLVM via {@see StringHashEquals} / {@see StringHashHmacAlgos} (#7189 phase 1).
*/
final class StringHashCryptoJit
{
Expand All @@ -28,7 +28,10 @@ final class StringHashCryptoJit

public static function implement(Context $context): void
{
// AOT standalone resolves __compiler_hash* via Linker (hash_crypto_jit_runtime.c).
StringHashEquals::ensureLinked($context);
StringHashHmacAlgos::ensureLinked($context);

// AOT standalone: digest symbols still come from Linker (hash_crypto_jit_runtime.c).
if (Builtin::LOAD_TYPE_STANDALONE === $context->loadType) {
return;
}
Expand Down
117 changes: 117 additions & 0 deletions lib/JIT/Builtin/StringHashEquals.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\JIT\Builtin;

use PHPCompiler\JIT\Context;
use PHPLLVM\Builder;
use PHPLLVM\Value;
use PHPLLVM\Value\Function_ as LlvmFunction;

/**
* LLVM implementation of __compiler_hash_equals (issue #7189 phase 1).
*
* php-src: ext/hash/hash.c — timing-safe compare for hash_equals().
* VM semantics: ext/standard/VmHash::equals().
*/
final class StringHashEquals
{
public static function ensureLinked(Context $context): void
{
self::implement($context);
}

public static function implement(Context $context): void
{
$probe = $context->module->getNamedFunction('__compiler_hash_equals');
if (null !== $probe && $probe->countBasicBlocks() > 0) {
$context->registerFunction('__compiler_hash_equals', $probe);

return;
}

$strPtr = $context->getTypeFromString('__string__*');
$i32 = $context->getTypeFromString('int32');
$ft = $context->context->functionType($i32, false, $strPtr, $strPtr);
$fn = null !== $probe
? $probe
: $context->module->addFunction('__compiler_hash_equals', $ft);
self::implementHashEquals($context, $fn);
$context->registerFunction('__compiler_hash_equals', $fn);
}

private static function implementHashEquals(Context $context, LlvmFunction $fn): void
{
$entry = $fn->appendBasicBlock('hash_equals_entry');
$context->builder->positionAtEnd($entry);

$known = $fn->getParam(0);
$user = $fn->getParam(1);
$i32 = $context->getTypeFromString('int32');
$i64 = $context->getTypeFromString('int64');
$zeroI32 = $i32->constInt(0, false);
$oneI32 = $i32->constInt(1, false);
$zeroI64 = $i64->constInt(0, false);
$oneI64 = $i64->constInt(1, false);

$strlen = $context->lookupFunction('__string__strlen');
$knownLen = $context->builder->call($strlen, $known);
$userLen = $context->builder->call($strlen, $user);
$lenMismatch = $context->builder->icmp(Builder::INT_NE, $knownLen, $userLen);

$failBb = $fn->appendBasicBlock('hash_equals_fail');
$loopHead = $fn->appendBasicBlock('hash_equals_loop_head');
$context->builder->branchIf($lenMismatch, $failBb, $loopHead);

$context->builder->positionAtEnd($failBb);
$context->builder->returnValue($zeroI32);
$context->builder->clearInsertionPosition();

$knownData = self::stringData($context, $known);
$userData = self::stringData($context, $user);
$iSlot = $context->builder->alloca($i64, 1, 'hash_equals_i');
$accSlot = $context->builder->alloca($i32, 1, 'hash_equals_acc');
$context->builder->store($zeroI64, $iSlot);
$context->builder->store($zeroI32, $accSlot);

$context->builder->positionAtEnd($loopHead);
$i = $context->builder->load($iSlot);
$done = $context->builder->icmp(Builder::INT_SGE, $i, $knownLen);
$loopBody = $fn->appendBasicBlock('hash_equals_loop_body');
$loopDone = $fn->appendBasicBlock('hash_equals_loop_done');
$context->builder->branchIf($done, $loopDone, $loopBody);

$context->builder->positionAtEnd($loopBody);
$ka = $context->builder->load($context->builder->gep($knownData, $i));
$ua = $context->builder->load($context->builder->gep($userData, $i));
$xor = $context->builder->xor(
$context->builder->zExt($ka, $i32),
$context->builder->zExt($ua, $i32)
);
$acc = $context->builder->load($accSlot);
$context->builder->store($context->builder->or($acc, $xor), $accSlot);
$context->builder->store($context->builder->addNoSignedWrap($i, $oneI64), $iSlot);
$context->builder->branch($loopHead);

$context->builder->positionAtEnd($loopDone);
$finalAcc = $context->builder->load($accSlot);
$isZero = $context->builder->icmp(Builder::INT_EQ, $finalAcc, $zeroI32);
$okBb = $fn->appendBasicBlock('hash_equals_ok');
$context->builder->branchIf($isZero, $okBb, $failBb);

$context->builder->positionAtEnd($okBb);
$context->builder->returnValue($oneI32);
$context->builder->clearInsertionPosition();
}

private static function stringData(Context $context, Value $str): Value
{
$map = $context->structFieldMap['__string__'];

return $context->builder->pointerCast(
$context->builder->structGep($str, $map['value']),
$context->getTypeFromString('int8*')
);
}
}
90 changes: 90 additions & 0 deletions lib/JIT/Builtin/StringHashHmacAlgos.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\JIT\Builtin;

use PHPCompiler\JIT\Context;
use PHPLLVM\Builder;
use PHPLLVM\Value\Function_ as LlvmFunction;

/**
* LLVM implementation of __compiler_hash_hmac_algos (issue #7189 phase 1).
*
* php-src: ext/hash/hash.c — HMAC-capable digest names.
* VM semantics: ext/standard/VmHash::hmacAlgos().
*/
final class StringHashHmacAlgos
{
/** @var list<string> */
private const HMAC_ALGOS = ['md5', 'sha1', 'sha256'];

public static function ensureLinked(Context $context): void
{
self::implement($context);
}

public static function implement(Context $context): void
{
$probe = $context->module->getNamedFunction('__compiler_hash_hmac_algos');
if (null !== $probe && $probe->countBasicBlocks() > 0) {
$context->registerFunction('__compiler_hash_hmac_algos', $probe);

return;
}

$htPtr = $context->getTypeFromString('__hashtable__*');
$ft = $context->context->functionType($htPtr, false);
$fn = null !== $probe
? $probe
: $context->module->addFunction('__compiler_hash_hmac_algos', $ft);
self::implementHashHmacAlgos($context, $fn);
$context->registerFunction('__compiler_hash_hmac_algos', $fn);
}

private static function implementHashHmacAlgos(Context $context, LlvmFunction $fn): void
{
$entry = $fn->appendBasicBlock('hash_hmac_algos_entry');
$context->builder->positionAtEnd($entry);

$htPtr = $context->getTypeFromString('__hashtable__*');
$i64 = $context->getTypeFromString('int64');
$ht = $context->builder->call($context->lookupFunction('__hashtable__alloc'));
$nullHt = $htPtr->constNull();
$isNull = $context->builder->icmp(Builder::INT_EQ, $ht, $nullHt);

$failBb = $fn->appendBasicBlock('hash_hmac_algos_fail');
$buildBb = $fn->appendBasicBlock('hash_hmac_algos_build');
$context->builder->branchIf($isNull, $failBb, $buildBb);

$context->builder->positionAtEnd($failBb);
$context->builder->returnValue($nullHt);
$context->builder->clearInsertionPosition();

$context->builder->positionAtEnd($buildBb);
$setAt = $context->lookupFunction('__hashtable__setStringAt');
foreach (self::HMAC_ALGOS as $index => $algo) {
$context->builder->call(
$setAt,
$ht,
$i64->constInt($index, false),
self::literalString($context, $algo)
);
}
$context->builder->returnValue($ht);
$context->builder->clearInsertionPosition();
}

private static function literalString(Context $context, string $text): \PHPLLVM\Value
{
$i64 = $context->getTypeFromString('int64');
$charPtr = $context->getTypeFromString('char*');
$cstr = $context->builder->pointerCast($context->constantFromString($text), $charPtr);

return $context->builder->call(
$context->lookupFunction('__string__init'),
$i64->constInt(\strlen($text), false),
$cstr
);
}
}
38 changes: 0 additions & 38 deletions lib/JIT/Builtin/hash_crypto_jit_runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -760,41 +760,3 @@ __string__ *__compiler_hash_pbkdf2(
return result;
}

/** Timing-safe string compare for hash_equals() (issue #2179). */
int __compiler_hash_equals(__string__ *known, __string__ *user)
{
size_t known_len = hc_strlen(known);
size_t user_len = hc_strlen(user);
const unsigned char *ka = (const unsigned char *) hc_strdata(known);
const unsigned char *ua = (const unsigned char *) hc_strdata(user);
size_t len = known_len;
int result = 0;

if (known_len != user_len) {
return 0;
}
for (size_t i = 0; i < len; i++) {
result |= ka[i] ^ ua[i];
}

return result == 0 ? 1 : 0;
}

/** hash_hmac_algos() — HMAC-capable digest names (ext/hash/hash.c, issue #6229). */
__hashtable__ *__compiler_hash_hmac_algos(void)
{
static const char *algos[] = {"md5", "sha1", "sha256", NULL};
__hashtable__ *ht;
size_t i;

ht = __hashtable__alloc();
if (NULL == ht) {
return NULL;
}
for (i = 0; algos[i] != NULL; ++i) {
size_t len = strlen(algos[i]);
__hashtable__setStringAt(ht, i, __string__init((long long) len, algos[i]));
}

return ht;
}
7 changes: 7 additions & 0 deletions test/unit/JIT/StringHashCryptoRuntimeStandaloneTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ public function testRuntimeShrinkRemovesHashCryptoC(): void
$this->assertFileExists(__DIR__.'/../../../lib/JIT/Builtin/hash_crypto_jit_runtime.c');
$jit = (string) file_get_contents(__DIR__.'/../../../lib/JIT/Builtin/StringHashCryptoJit.php');
$this->assertStringContainsString('__compiler_hash', $jit);
$this->assertStringContainsString('StringHashEquals', $jit);
$this->assertStringContainsString('StringHashHmacAlgos', $jit);
$this->assertStringContainsString('StringHashCryptoJit', $jit);
$linker = (string) file_get_contents(__DIR__.'/../../../lib/AOT/Linker.php');
$this->assertStringNotContainsString('runtime/hash_crypto.c', $linker);
$this->assertStringContainsString('hash_crypto_jit_runtime.c', $linker);
$runtimeC = (string) file_get_contents(__DIR__.'/../../../lib/JIT/Builtin/hash_crypto_jit_runtime.c');
$this->assertStringNotContainsString('__compiler_hash_equals', $runtimeC);
$this->assertStringNotContainsString('__compiler_hash_hmac_algos', $runtimeC);
$equalsJit = (string) file_get_contents(__DIR__.'/../../../lib/JIT/Builtin/StringHashEquals.php');
$this->assertStringContainsString('__compiler_hash_equals', $equalsJit);
$runtime = (string) file_get_contents(__DIR__.'/../../../lib/JIT/Builtin/StringHashCrypto.php');
$this->assertStringContainsString('StringHashCryptoJit', $runtime);
$this->assertStringNotContainsString('hash_crypto.c', $runtime);
Expand Down