diff --git a/ext/ldap/JitLdapLink.php b/ext/ldap/JitLdapLink.php index 860f8ef3a78..9f95b68cef1 100644 --- a/ext/ldap/JitLdapLink.php +++ b/ext/ldap/JitLdapLink.php @@ -18,7 +18,7 @@ use PHPLLVM\Builder; use PHPLLVM\Value; -/** LLVM lowering for ldap_bind() / ldap_bind_ext() / ldap_sasl_bind() / ldap_unbind() / ldap_close() / ldap_set/get_option() / ldap_start_tls() / ldap_set_rebind_proc() (#32001, #32002, #32107, #32109, #32146, #32147, #32148). */ +/** LLVM lowering for ldap_bind() / ldap_bind_ext() / ldap_sasl_bind() / ldap_unbind() / ldap_close() / ldap_set/get_option() / ldap_start_tls() / ldap_set_rebind_proc() (#32001, #32002, #32107, #32109, #32146, #32147, #32148, #32172). */ final class JitLdapLink { /** @param list $args */ @@ -186,13 +186,16 @@ public static function invokeBindExt(Context $context, array $args): Value BasicBlockHelper::restoreInsertBlock($context, $savedInsert); } - return $context->builder->call( - $context->lookupFunction('__compiler_ldap_bind_ext'), - $handle, - $dn, - $password, - $hasDn, - $hasPassword + return JitLdapResult::registerReturnedResult( + $context, + $context->builder->call( + $context->lookupFunction('__compiler_ldap_bind_ext'), + $handle, + $dn, + $password, + $hasDn, + $hasPassword + ) ); } diff --git a/ext/ldap/JitLdapResult.php b/ext/ldap/JitLdapResult.php index 55d9ebead37..36f908871d3 100644 --- a/ext/ldap/JitLdapResult.php +++ b/ext/ldap/JitLdapResult.php @@ -12,9 +12,11 @@ use PHPCompiler\JIT\JitStringBuiltinArg; use PHPCompiler\JIT\JitValueBox; use PHPCompiler\JIT\Variable as JITVariable; +use PHPCompiler\VM\Variable; +use PHPLLVM\Builder; use PHPLLVM\Value; -/** LLVM lowering for ldap_compare() (#32121). */ +/** LLVM lowering for ldap_compare() / ldap_count_entries() (#32121, #32172). */ final class JitLdapResult { /** @param list $args */ @@ -48,6 +50,112 @@ public static function invokeCompare(Context $context, array $args): Value ); } + /** @param list $args */ + public static function invokeCountEntries(Context $context, array $args): Value + { + $argc = \count($args); + if (2 !== $argc) { + throw new \ArgumentCountError(\sprintf( + 'ldap_count_entries() expects exactly 2 arguments, %d given', + $argc + )); + } + + $conn = self::lowerConnectionHandle($context, $args[0], 'ldap_count_entries'); + $result = self::lowerResultHandle($context, $args[1], 'ldap_count_entries'); + + $savedInsert = BasicBlockHelper::tryGetInsertBlock($context); + LdapRuntime::ensureLinked($context); + if (null !== $savedInsert) { + BasicBlockHelper::restoreInsertBlock($context, $savedInsert); + } + + $count = $context->builder->call( + $context->lookupFunction('__compiler_ldap_count_entries'), + $conn, + $result + ); + + return self::longFromI64($context, $count); + } + + /** + * Map a NestedJIT LDAP\Result return value to its VM result id (#32172). + * + * Peer: ldap_connect() register in {@see ldap_connect::call}. + */ + public static function registerReturnedResult(Context $context, Value $result): Value + { + $typeField = $context->structFieldMap['__value__']['type']; + $typeByte = $context->builder->load( + $context->builder->structGep($result, $typeField) + ); + $i8 = $context->getTypeFromString('int8'); + $kind = $context->builder->and($typeByte, $i8->constInt(0x7f, false)); + $isObject = $context->builder->icmp( + Builder::INT_EQ, + $kind, + $i8->constInt(Variable::TYPE_OBJECT & 0x7f, false) + ); + $regBb = BasicBlockHelper::append($context, 'ldap_result_register'); + $doneBb = BasicBlockHelper::append($context, 'ldap_result_done'); + $context->builder->branchIf($isObject, $regBb, $doneBb); + + $context->builder->positionAtEnd($regBb); + $obj = $context->builder->call( + $context->lookupFunction('__value__readObject'), + $result + ); + $voidp = $context->getTypeFromString('void')->pointerType(0); + $i64 = $context->getTypeFromString('int64'); + $objAddr = $context->builder->ptrToInt( + $context->builder->pointerCast($obj, $voidp), + $i64 + ); + $context->builder->call( + $context->lookupFunction('__compiler_ldap_result_register'), + $objAddr + ); + $context->builder->branch($doneBb); + $context->builder->positionAtEnd($doneBb); + + return $result; + } + + private static function longFromI64(Context $context, Value $value): Value + { + $slot = JitValueBox::alloc($context); + $ptr = JitValueBox::pointer($context, $slot); + JitValueBox::writeLong($context, $slot, $value); + + return $ptr; + } + + private static function lowerResultHandle(Context $context, JITVariable $arg, string $function): Value + { + if (JITVariable::TYPE_OBJECT === $arg->type) { + return JitGetObjectId::invoke($context, $arg, $function); + } + if (JITVariable::TYPE_VALUE === $arg->type) { + $loaded = JitValueBox::valuePtrFromVariable($context, $arg); + $obj = $context->builder->call( + $context->lookupFunction('__value__readObject'), + $loaded + ); + $voidp = $context->getTypeFromString('void')->pointerType(0); + $i64 = $context->getTypeFromString('int64'); + + return $context->builder->ptrToInt( + $context->builder->pointerCast($obj, $voidp), + $i64 + ); + } + + self::emitTypeErrorAndAbort($context, self::resultScalarTypeError($function, $arg->type)); + + return $context->getTypeFromString('int64')->constInt(0, false); + } + private static function lowerConnectionHandle(Context $context, JITVariable $arg, string $function): Value { if (JITVariable::TYPE_OBJECT === $arg->type) { @@ -82,28 +190,46 @@ private static function emitTypeErrorAndAbort(Context $context, string $message) } private static function scalarTypeError(string $function, int $type): string + { + return self::typeErrorMessage($function, 1, 'ldap', 'LDAP\\Connection', self::typeLabel($type)); + } + + private static function resultScalarTypeError(string $function, int $type): string + { + return self::typeErrorMessage($function, 2, 'result', 'LDAP\\Result', self::typeLabel($type)); + } + + private static function typeLabel(int $type): string { switch ($type) { case JITVariable::TYPE_NATIVE_LONG: - return self::typeErrorMessage($function, 'int'); + return 'int'; case JITVariable::TYPE_NATIVE_DOUBLE: - return self::typeErrorMessage($function, 'float'); + return 'float'; case JITVariable::TYPE_NATIVE_BOOL: - return self::typeErrorMessage($function, 'bool'); + return 'bool'; case JITVariable::TYPE_STRING: - return self::typeErrorMessage($function, 'string'); + return 'string'; case JITVariable::TYPE_NULL: - return self::typeErrorMessage($function, 'null'); + return 'null'; default: - return self::typeErrorMessage($function, 'mixed'); + return 'mixed'; } } - private static function typeErrorMessage(string $function, string $given): string - { + private static function typeErrorMessage( + string $function, + int $argNum, + string $param, + string $expected, + string $given + ): string { return \sprintf( - '%s(): Argument #1 ($ldap) must be of type LDAP\\Connection, %s given', + '%s(): Argument #%d ($%s) must be of type %s, %s given', $function, + $argNum, + $param, + $expected, $given ); } diff --git a/ext/ldap/LdapLinkJitHelper.php b/ext/ldap/LdapLinkJitHelper.php index e5e54521811..992acfc1318 100644 --- a/ext/ldap/LdapLinkJitHelper.php +++ b/ext/ldap/LdapLinkJitHelper.php @@ -60,6 +60,7 @@ public static function bindExtArgv(int $handle, ?string $dn, ?string $password, return $out; } + VmLdapResult::enqueuePendingJitHandle($result->toObject()->id); return $result; } diff --git a/ext/ldap/LdapResultJitHelper.php b/ext/ldap/LdapResultJitHelper.php index e10159b5bca..bced10b61f5 100644 --- a/ext/ldap/LdapResultJitHelper.php +++ b/ext/ldap/LdapResultJitHelper.php @@ -8,13 +8,18 @@ use PHPCompiler\VM\Variable; /** - * ldap_compare() for compiled JIT/AOT modules (#32121). + * ldap_compare() / ldap_count_entries() for compiled JIT/AOT modules (#32121, #32172). * - * SSOT: {@see VmLdapCore::compare} - * php-src: ext/ldap/ldap.c — PHP_FUNCTION(ldap_compare) + * SSOT: {@see VmLdapCore::compare} / {@see VmLdapNative::countEntries} + * php-src: ext/ldap/ldap.c — PHP_FUNCTION(ldap_compare) / ldap_count_entries */ final class LdapResultJitHelper { + public static function registerHandleArgv(int $handle): void + { + VmLdapResult::claimPendingJitHandle($handle); + } + public static function compareArgv(int $handle, string $dn, string $attribute, string $value): Variable { $conn = self::requireConnection($handle, 'ldap_compare'); @@ -29,6 +34,17 @@ public static function compareArgv(int $handle, string $dn, string $attribute, s return $out; } + public static function countEntriesArgv(int $connHandle, int $resultHandle): int + { + $conn = self::requireConnection($connHandle, 'ldap_count_entries'); + $result = self::requireResult($resultHandle, 'ldap_count_entries'); + + return VmLdapNative::countEntries( + VmLdapConnection::native($conn), + VmLdapResult::resultNative($result) + ); + } + private static function requireConnection(int $handle, string $function): ObjectEntry { if (VmLdapConnection::isClosedLookupKey($handle)) { @@ -45,4 +61,21 @@ private static function requireConnection(int $handle, string $function): Object return $conn; } + + private static function requireResult(int $handle, string $function): ObjectEntry + { + if (VmLdapResult::isFreedLookupKey($handle)) { + throw new \TypeError( + $function.'(): supplied LDAP\\Result is not a valid ldap result resource' + ); + } + $result = VmLdapResult::resultForLookupKey($handle); + if (null === $result) { + throw new \TypeError( + $function.'(): Argument #2 ($result) must be of type LDAP\\Result, mixed given' + ); + } + + return $result; + } } diff --git a/ext/ldap/VmLdapResult.php b/ext/ldap/VmLdapResult.php index 4232451c0a5..6144d9cf4f1 100644 --- a/ext/ldap/VmLdapResult.php +++ b/ext/ldap/VmLdapResult.php @@ -22,12 +22,18 @@ final class VmLdapResult public const ENTRY_CLASS_NAME = 'LDAP\\ResultEntry'; - /** @var array */ + /** @var array */ private static array $results = []; /** @var array */ private static array $entries = []; + /** @var list object ids from wrapResult awaiting JIT handle registration (#32172) */ + private static array $pendingJitHandleIds = []; + + /** @var array JIT object address (ptrToInt) => object id */ + private static array $jitHandleToId = []; + public static function registerClasses(Context $ctx): void { if (!isset($ctx->classes[self::RESULT_CLASS_LC])) { @@ -51,6 +57,7 @@ public static function wrapResult(\FFI\CData $native, Context $ctx, ObjectEntry 'native' => $native, 'freed' => false, 'connection_id' => $connection->id, + 'object' => $object, ]; $var = new Variable(Variable::TYPE_OBJECT); $var->object($object); @@ -58,6 +65,51 @@ public static function wrapResult(\FFI\CData $native, Context $ctx, ObjectEntry return $var; } + /** Enqueue object id after ldap_bind_ext() NestedJIT helper wrap (#32172). */ + public static function enqueuePendingJitHandle(int $objectId): void + { + self::$pendingJitHandleIds[] = $objectId; + } + + /** Map compiled __object__* address to VM result state after Result wrap JIT (#32172). */ + public static function claimPendingJitHandle(int $handle): void + { + if ($handle <= 0 || [] === self::$pendingJitHandleIds) { + return; + } + self::$jitHandleToId[$handle] = (int) \array_shift(self::$pendingJitHandleIds); + } + + public static function resultForLookupKey(int $handle): ?ObjectEntry + { + if ($handle <= 0) { + return null; + } + $id = self::$jitHandleToId[$handle] ?? null; + if (null === $id || !isset(self::$results[$id])) { + return null; + } + $object = self::$results[$id]['object']; + if (!self::isLiveResult($object)) { + return null; + } + + return $object; + } + + public static function isFreedLookupKey(int $handle): bool + { + if ($handle <= 0) { + return false; + } + $id = self::$jitHandleToId[$handle] ?? null; + if (null === $id || !isset(self::$results[$id])) { + return false; + } + + return (bool) self::$results[$id]['freed']; + } + public static function wrapEntry(\FFI\CData $native, Context $ctx, ObjectEntry $connection, int $resultId): Variable { self::registerClasses($ctx); diff --git a/ext/ldap/ldap_search_builtins.php b/ext/ldap/ldap_search_builtins.php index 46480f9c190..f6899a33ec8 100644 --- a/ext/ldap/ldap_search_builtins.php +++ b/ext/ldap/ldap_search_builtins.php @@ -177,7 +177,7 @@ public function execute(Frame $frame): void public function call(Context $context, JITVariable ...$args): Value { - throw new \LogicException('ldap_count_entries() is not implemented for JIT in this compiler build (issue #3369)'); + return JitLdapResult::invokeCountEntries($context, $args); } } diff --git a/lib/JIT/Builtin/LdapRuntime.php b/lib/JIT/Builtin/LdapRuntime.php index 10e1d8bf4d7..c2ae9d3a7d4 100644 --- a/lib/JIT/Builtin/LdapRuntime.php +++ b/lib/JIT/Builtin/LdapRuntime.php @@ -11,8 +11,8 @@ * JIT/AOT link for ldap_escape / ldap_dn2ufn / ldap_explode_dn / ldap_connect / * ldap_connect_wallet / ldap_bind / ldap_bind_ext / ldap_unbind / ldap_errno / ldap_error / * ldap_err2str / ldap_set_option / ldap_get_option / ldap_start_tls / ldap_sasl_bind / - * ldap_compare / ldap_set_rebind_proc - * (#6352, #18173, #22212, #22276, #31984, #32000, #32001, #32002, #32106, #32107, #32109, #32121, #32146, #32147, #32148). + * ldap_compare / ldap_set_rebind_proc / ldap_count_entries + * (#6352, #18173, #22212, #22276, #31984, #32000, #32001, #32002, #32106, #32107, #32109, #32121, #32146, #32147, #32148, #32172). * * Helper compile: {@see JitVmHelperLink::ensureBridge} (peer StringStrcoll #22256). * php-src: ext/ldap/ldap.c @@ -65,6 +65,10 @@ final class LdapRuntime private const LDAP_COMPARE_HELPER = 'PHPCompiler\\ext\\ldap\\LdapResultJitHelper::compareArgv'; + private const LDAP_RESULT_REGISTER_HELPER = 'PHPCompiler\\ext\\ldap\\LdapResultJitHelper::registerHandleArgv'; + + private const LDAP_COUNT_ENTRIES_HELPER = 'PHPCompiler\\ext\\ldap\\LdapResultJitHelper::countEntriesArgv'; + /** @var list */ private const ESCAPE_HELPERS = [ self::LDAP_ESCAPE_HELPER, @@ -98,6 +102,8 @@ final class LdapRuntime /** @var list */ private const RESULT_HELPERS = [ self::LDAP_COMPARE_HELPER, + self::LDAP_RESULT_REGISTER_HELPER, + self::LDAP_COUNT_ENTRIES_HELPER, ]; public static function ensureLinked(Context $context): void @@ -331,6 +337,28 @@ private static function implement(Context $context): void self::RESULT_HELPERS, '#32121' ); + JitVmHelperLink::ensureBridge( + $context, + '__compiler_ldap_result_register', + 'ldap_result_register_bridge_entry', + [$i64], + $context->getTypeFromString('void'), + self::LDAP_RESULT_REGISTER_HELPER, + self::RESULT_HELPER_PATH, + self::RESULT_HELPERS, + '#32172' + ); + JitVmHelperLink::ensureBridge( + $context, + '__compiler_ldap_count_entries', + 'ldap_count_entries_bridge_entry', + [$i64, $i64], + $i64, + self::LDAP_COUNT_ENTRIES_HELPER, + self::RESULT_HELPER_PATH, + self::RESULT_HELPERS, + '#32172' + ); if (null !== $savedBlock) { $context->builder->positionAtEnd($savedBlock); diff --git a/test/repro/issue_32172_ldap_count_entries_jit.php b/test/repro/issue_32172_ldap_count_entries_jit.php new file mode 100644 index 00000000000..e0323e6c95b --- /dev/null +++ b/test/repro/issue_32172_ldap_count_entries_jit.php @@ -0,0 +1,52 @@ +getMessage(), 'LDAP\\Connection') ? "bad_conn=typeerror\n" : "bad_conn=other\n"; +} + +try { + ldap_count_entries($link, 43); + echo "bad_result=uncaught\n"; +} catch (TypeError $e) { + echo str_contains($e->getMessage(), 'LDAP\\Result') ? "bad_result=typeerror\n" : "bad_result=other\n"; +} + +$r = @ldap_bind_ext($link); +if ($r instanceof LDAP\Result) { + $n = @ldap_count_entries($link, $r); + echo 'count=', is_int($n) ? 'int' : 'other', PHP_EOL; +} else { + echo "bind_ext=false\n"; +} + +echo "ok\n"; diff --git a/test/unit/LdapCountEntriesJitHelperTest.php b/test/unit/LdapCountEntriesJitHelperTest.php new file mode 100644 index 00000000000..7e53f007fd3 --- /dev/null +++ b/test/unit/LdapCountEntriesJitHelperTest.php @@ -0,0 +1,102 @@ +assertStringContainsString('JitLdapResult::invokeCountEntries', $source); + $this->assertStringNotContainsString('ldap_count_entries() is not implemented for JIT', $source); + + $jit = (string) file_get_contents(__DIR__.'/../../ext/ldap/JitLdapResult.php'); + $this->assertStringContainsString('__compiler_ldap_count_entries', $jit); + $this->assertStringContainsString('__compiler_ldap_result_register', $jit); + + $runtime = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/LdapRuntime.php'); + $this->assertStringContainsString('LdapResultJitHelper::countEntriesArgv', $runtime); + $this->assertStringContainsString('LdapResultJitHelper::registerHandleArgv', $runtime); + $this->assertStringContainsString('__compiler_ldap_count_entries', $runtime); + } + + public function testTypeErrorOnNonConnectionHandle(): void + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('ldap_count_entries(): Argument #1 ($ldap) must be of type LDAP\\Connection, mixed given'); + LdapResultJitHelper::countEntriesArgv(888_889, 888_890); + } + + public function testTypeErrorOnNonResultHandle(): void + { + if (!VmLdapNative::available()) { + self::markTestSkipped('libldap FFI absent — count_entries path not exercised'); + } + $ctx = self::ldapContext(); + $linkVar = VmLdapCore::connect('ldap://127.0.0.1', null, $ctx); + if (false === $linkVar) { + self::markTestSkipped('ldap_connect failed in container'); + } + $object = $linkVar->toObject(); + VmLdapConnection::enqueuePendingJitHandle($object->id); + VmLdapConnection::claimPendingJitHandle(42_172); + + try { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('ldap_count_entries(): Argument #2 ($result) must be of type LDAP\\Result, mixed given'); + LdapResultJitHelper::countEntriesArgv(42_172, 999_991); + } finally { + VmLdapConnection::close($object); + } + } + + public function testCountEntriesOnBindExtResultWithoutLiveDirectory(): void + { + if (!VmLdapNative::available()) { + self::markTestSkipped('libldap FFI absent — count_entries path not exercised'); + } + $ctx = self::ldapContext(); + $linkVar = VmLdapCore::connect('ldap://127.0.0.1', null, $ctx); + if (false === $linkVar) { + self::markTestSkipped('ldap_connect failed in container'); + } + $object = $linkVar->toObject(); + VmLdapConnection::enqueuePendingJitHandle($object->id); + VmLdapConnection::claimPendingJitHandle(42_173); + + \PHPCompiler\Web\Superglobals::setActiveContext($ctx); + set_error_handler(static fn (): bool => true); + try { + $out = \PHPCompiler\ext\ldap\LdapLinkJitHelper::bindExtArgv(42_173, null, null, 0, 0); + if (\PHPCompiler\VM\Variable::TYPE_OBJECT !== $out->type) { + self::markTestSkipped('anonymous bind_ext did not return LDAP\\Result'); + } + VmLdapResult::claimPendingJitHandle(42_174); + $count = LdapResultJitHelper::countEntriesArgv(42_173, 42_174); + $this->assertIsInt($count); + } finally { + restore_error_handler(); + VmLdapConnection::close($object); + } + } + + private static function ldapContext(): Context + { + $runtime = new Runtime(); + $runtime->load(new \PHPCompiler\ext\ldap\Module()); + + return $runtime->vmContext; + } +} diff --git a/test/unit/LdapRuntimeShrinkTest.php b/test/unit/LdapRuntimeShrinkTest.php index ba437b0c905..34223bc744f 100644 --- a/test/unit/LdapRuntimeShrinkTest.php +++ b/test/unit/LdapRuntimeShrinkTest.php @@ -33,6 +33,9 @@ public function testLdapRuntimeUsesJitVmHelperLinkForAllBridges(): void $this->assertStringContainsString('LdapLinkJitHelper::getOptionValueArgv', $source); $this->assertStringContainsString('LdapLinkJitHelper::startTlsArgv', $source); $this->assertStringContainsString('LdapLinkJitHelper::setRebindProcClearArgv', $source); + $this->assertStringContainsString('LdapResultJitHelper::compareArgv', $source); + $this->assertStringContainsString('LdapResultJitHelper::registerHandleArgv', $source); + $this->assertStringContainsString('LdapResultJitHelper::countEntriesArgv', $source); $this->assertStringContainsString('JitVmHelperLink::ensureBridge', $source); $this->assertStringContainsString('__compiler_ldap_escape', $source); $this->assertStringContainsString('__compiler_ldap_connect_wallet', $source); @@ -49,8 +52,10 @@ public function testLdapRuntimeUsesJitVmHelperLinkForAllBridges(): void $this->assertStringContainsString('__compiler_ldap_start_tls', $source); $this->assertStringContainsString('__compiler_ldap_set_rebind_proc', $source); $this->assertStringContainsString('__compiler_ldap_compare', $source); + $this->assertStringContainsString('__compiler_ldap_result_register', $source); + $this->assertStringContainsString('__compiler_ldap_count_entries', $source); $this->assertStringContainsString('ldap_connect_bridge_entry', $source); - $this->assertSame(19, \preg_match_all('/JitVmHelperLink::ensureBridge\(/', $source)); + $this->assertSame(21, \preg_match_all('/JitVmHelperLink::ensureBridge\(/', $source)); $this->assertStringNotContainsString('NestedJitCompileScope::run', $source); $this->assertStringNotContainsString('parseAndCompile', $source); $this->assertStringNotContainsString('new JIT(', $source);