Skip to content

Firebird Client Deadlock #9214

Open

Description

@slalom91

Title: Client (4.0+) calls LoadLibrary for missing key-holder plugin “NoDefault” on every attach while holding a global mutex, which can deadlock with the Windows loader lock

Environment

Client: fbclient.dll 5.0.2.1613, 32-bit (also present in 5.0.4.1812 and current master source)
Server: Firebird 5.0.4.1812 SuperServer, same host
OS: Windows Server 2016 (10.0.14393), 32-bit application under WOW64
fbclient.dll loaded from C:\Windows\SysWOW64, with no firebird.conf or plugins folder beside it
The same application with fbclient.dll 3.0.2.32703 has no problem: 13+ days of uptime under the same workload pattern.

Summary

A multi-threaded application deadlocked permanently inside fbclient.dll. Two full-memory dumps taken 78 seconds apart show identical state. Two threads are involved:

Thread A is attaching to a database. It is inside PluginSet::next(), holding loadModuleMutex, and blocked in LoadLibraryExA("C:\Windows\SYSTEM32\plugins\NoDefault") waiting for the OS loader lock:
ntdll!NtWaitForSingleObject
ntdll!LdrpDrainWorkQueue
ntdll!LdrpLoadDllInternal
ntdll!LdrpLoadDll
ntdll!LdrLoadDll
KERNELBASE!LoadLibraryExW
KERNELBASE!LoadLibraryExA <- "C:\Windows\SYSTEM32\plugins\NoDefault"
FBCLIENT!...
Thread B holds the loader lock because it is inside LoadLibraryW of an application DLL whose initialization calls isc_attach_database. It is blocked on a critical section in fbclient’s data section that Thread A owns. !locks reports OwningThread = Thread A.
ntdll!RtlEnterCriticalSection <- CS in FBCLIENT data section, owned by Thread A
FBCLIENT!...
FBCLIENT!isc_attach_database

ntdll!LdrpCallInitRoutine
ntdll!LdrpInitializeNode
...
ntdll!LdrLoadDll
KERNELBASE!LoadLibraryW

We accept that attaching from DLL initialization, under the loader lock, is wrong on our side, and we are removing it. We’re reporting this because the client behavior turns a latent mistake into an intermittent hard hang, and because the same lock-plus-LoadLibrary pattern could also interact badly with other loader activity in the host process.

Analysis (from source, v5.0.2)

src/remote/remote.h, ClntAuthBlock::ClientCrypt constructs GetPlugins(TYPE_KEY_HOLDER, "NoDefault"). This came in with CORE-6220 (commit 0453bbe, first released in 4.0). As a result, every remote attach looks up a key-holder plugin named NoDefault.
No such plugin is registered. PluginSet::next() (src/yvalve/PluginManager.cpp) therefore calls loadModule(), which tries ModuleLoader::loadModule() on /plugins/NoDefault and its extension variants.
A failed load isn’t cached, so this LoadLibrary attempt repeats on every attach.
Since commit 791d46b (first in 4.0), PluginSet::next() holds a static loadModuleMutex across loadModule(). Every attach therefore does LoadLibrary, which needs the loader lock, while holding a process-wide client mutex. Any thread that holds the loader lock and then enters the client deadlocks against it.

Neither change is in 3.0.x, which matches our experience that 3.0.2 doesn’t show the problem. Both are unchanged in 5.0.4 and master.

Suggestions

Skip the key-holder lookup when no KeyHolderPlugin is configured, or treat the NoDefault placeholder specially so it never reaches ModuleLoader.
Cache negative results for modules that failed to load, so a missing plugin isn’t retried with LoadLibrary on every attach.
Consider not holding loadModuleMutex across the actual LoadLibrary call, for example with a per-module “loading” marker and a condition wait.

Reproduction

We haven’t built a minimal standalone repro. The deadlock is timing-dependent; it appeared after about 18 hours in production. The ingredients are:

One thread repeatedly doing remote attaches through a 4.0+ client.
Another thread calling LoadLibrary on a DLL whose initialization calls isc_attach_database.

Activity

  1. mariuz commented on Oct 20, 2026

    @mariuz
    Member

    Thank you for the detailed report and root-cause analysis!

    Confirmed on master (affecting 4.0, 5.0, and 6.0):

    1. Unintended "NoDefault" plugin lookup on every attach:
      In src/remote/remote.h (ClntAuthBlock::ClientCrypt), the constructor initializes:

      ClientCrypt()
          : pluginItr(Firebird::IPluginManager::TYPE_KEY_HOLDER, "NoDefault")
      { }

      This was introduced in 0453bbec9e (CORE-6220) as a placeholder before ClientCrypt::create(const Config* conf) later calls pluginItr.set(conf). However, GetPlugins only treats nullptr specially (falling back to the default config) and passes any non-null string directly to PluginManager::getPlugins(). As a result, "NoDefault" is treated as a literal plugin name, causing PluginSet::next() to call ModuleLoader::loadModule (LoadLibraryEx / dlopen) for plugins/NoDefault while holding loadModuleMutex on every ClntAuthBlock construction, right before pluginItr.set(clntConfig) replaces the iterator with the configured KeyHolderPlugin (which defaults to "").

      Passing "" (empty list) instead of "NoDefault" avoids the bogus module load completely.

    2. Unconditional loadModuleMutex lock in PluginSet::next():
      In src/yvalve/PluginManager.cpp, PluginSet::next() acquires loadModuleMutex and plugins->mutex before checking whether namesList has any remaining plugin names (e.g., when KeyHolderPlugin is the default ""), and even when the requested plugin is already loaded in memory in plugins. Checking currentName.getWord(namesList, " \t,;") before locking and checking plugins->get(...) before acquiring loadModuleMutex eliminates the unnecessary loadModuleMutex lock.

    Proposed fix in #9224.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions