I am working on a Windows DLL which is being loaded by a 3rd party executable. It has a stop functionality which unloads my DLL. I can see that while running in Visual Studio's debugger:
'3rdparty.exe' (Win32): Unloaded 'C:\my_dll.dll'
Now, if I add this code:
const std::string& public_key = my_public_key;
auto bio = BIO_new(BIO_s_mem());
if (!BIO_write(bio, public_key.c_str(), static_cast<int>(public_key.size())))
{
error();
}
EVP_PKEY* pkey = nullptr;
if (!PEM_read_bio_PUBKEY(bio, &pkey, nullptr, nullptr) || pkey == nullptr)
{
error();
}
if (pkey)
{
EVP_PKEY_free(pkey);
}
if (bio)
BIO_free_all(bio);
Stopping my DLL, unloading doesn't happen anymore.
I tried a few things, like using a custom context:
OSSL_LIB_CTX* lib_ctx = OSSL_LIB_CTX_new();
if (!PEM_read_bio_PUBKEY_ex(bio, &pkey, nullptr, nullptr, lib_ctx, nullptr) || pkey == nullptr)
{
error();
}
...
OSSL_LIB_CTX_free(lib_ctx);
Also tried various clean up methods:
EVP_cleanup(); // Clean up EVP layer (legacy compatibility)
CRYPTO_cleanup_all_ex_data(); // Clean up extra data
ERR_free_strings(); // Free error strings
SSL_COMP_free_compression_methods(); // Free compression methods (if SSL is used)
But nothing helps.
I do not know exactly how the 3rd party does the stopping of my DLL, and I do not have access to that code.
SSL that I am using is: OpenSSL 3.2.1 30 Jan 2024
I statically link against libcrypto.lib
and libssl.lib
libraries.
By default, OpenSSL "pins" itself in memory so it can't be unloaded until process termination. However, until recently, this behavior was unconditional regardless of whether you used the static or dynamic version of OpenSSL. Which means, if you static link OpenSSL into your own DLL, then your DLL becomes pinned - just like you are experiencing.
There are some (closed) tickets on OpenSSL's GitHub repo about this issue:
#7598 dll can't be free after it call bio_new and bio_free APIs in windows platform
#20977 Static libcrypto uses DLL pinning
To address the issue, you need to:
use a version of the OpenSSL static libs which have been compiled with the no-shared
and no-pinshared
options.
have your code call OPENSSL_init_crypto()
with the OPENSSL_INIT_NO_ATEXIT
option (you will then have to call OPENSSL_cleanup()
manually).
This is documented in OpenSSL's install notes:
Enable and Disable Features
Feature options always come in pairs, an option to enable feature
xxxx
, and an option to disable it:
[ enable-xxxx | no-xxxx ]
Whether a feature is enabled or disabled by default, depends on the feature. In the following list, always the non-default variant is documented: if feature
xxxx
is disabled by default thenenable-xxxx
is documented and if featurexxxx
is enabled by default thenno-xxxx
is documented....
no-pinshared
Don't pin the shared libraries.
By default OpenSSL will attempt to stay in memory until the process exits. This is so that libcrypto and libssl can be properly cleaned up automatically via an
atexit()
handler. The handler is registered by libcrypto and cleans up both libraries. On some platforms theatexit()
handler will run on unload of libcrypto (if it has been dynamically loaded) rather than at process exit.This option can be used to stop OpenSSL from attempting to stay in memory until the process exits. This could lead to crashes if either libcrypto or libssl have already been unloaded at the point that the atexit handler is invoked, e.g. on a platform which calls
atexit()
on unload of the library, and libssl is unloaded before libcrypto then a crash is likely to happen.Note that shared library pinning is not automatically disabled for static builds, i.e.,
no-shared
does not implyno-pinshared
. This may come as a surprise when linking libcrypto statically into a shared third-party library, because in this case the shared library will be pinned. To prevent this behaviour, you need to configure the static build usingno-shared
andno-pinshared
together.Applications can suppress running of the
atexit()
handler at run time by using theOPENSSL_INIT_NO_ATEXIT
option toOPENSSL_init_crypto()
. See the man page for it for further details....
no-shared
Do not create shared libraries, only static ones.
...
Which means pinshared
is enabled by default, even in static builds.