程序关闭时析构函数中OpenSSL EVP_EncryptInit调用失败排查
问题
近期尝试改进崩溃处理器,记录对象销毁时的日志。为每个全局对象使用单例模式,并有函数按正确顺序初始化它们(崩溃处理器为最后一个)。但发现AES加密存在异常:程序正常运行时一切正常,仅在关闭程序、调用所有析构函数时出现问题。
在Application对象的析构函数中调用如下shutdown函数:
void shutdown() { dbg_println("Application shutdown"); secure_string key = "12345678901234567890123456789012"; secure_string iv = "1234567890123456"; secure_string out; int res = secure_crypto::aes_encrypt(key, iv, "shutdown", out); if (res != 0) { dbg_println("Application aes_encrypt failed {}", res); } }
Application对象是最后创建的,因此其析构函数最先被调用。
使用的AES加密代码如下:
using EVP_CIPHER_CTX_free_ptr = std::unique_ptr<EVP_CIPHER_CTX, decltype(&::EVP_CIPHER_CTX_free)>; int secure_crypto::aes_encrypt(const secure_string &key, const secure_string &iv, const secure_string &plainText, secure_string &encryptedText) { dbg_println("Encrypting {} with key {} and messageIv {}", plainText.size() > 500 ? "..." : secure_crypto::secure_string_to_hex(plainText), secure_crypto::secure_string_to_hex(key), secure_crypto::secure_string_to_hex(iv)); try { /* * Initialise the decryption operation. IMPORTANT - ensure you use a key * and IV size appropriate for your cipher * In this example we are using 256-bit AES (i.e. a 256-bit key). The * IV size for *most* modes is the same as the block size. For AES this * is 128 bits */ if (key.size() != 32 || iv.size() != 16) { return -1; } const auto *keyArr = (const unsigned char *) key.c_str(); const auto *ivArr = (const unsigned char *) iv.c_str(); EVP_CIPHER_CTX_free_ptr ctx(EVP_CIPHER_CTX_new(), ::EVP_CIPHER_CTX_free); secure_vector<unsigned char> keyV(keyArr, keyArr + key.size()); secure_vector<unsigned char> ivV(ivArr, ivArr + iv.size()); dbg_println("Encrypting with key {} and iv {} and status: {}", secure_crypto::secure_string_to_hex(keyV), secure_crypto::secure_string_to_hex(ivV), ctx.get() == nullptr ? "null" : "not null"); int rc = EVP_EncryptInit(ctx.get(), EVP_aes_256_cbc(), keyArr, ivArr); if (rc != 1) { #if LOG_CRYPTO_ERRORS printf("err: %s", ERR_error_string(ERR_get_error(), NULL)); dbg_println("[crypto] Error while aes encrypting: {}", rc); #endif return -2; } // Recovered text expands upto BLOCK_SIZE encryptedText.resize(plainText.size() + BLOCK_SIZE); int out_len1 = (int) encryptedText.size(); rc = EVP_EncryptUpdate(ctx.get(), (unsigned char *) &encryptedText[0], &out_len1, (const unsigned char *) &plainText[0], (int) plainText.size()); if (rc != 1) { return -3; } int out_len2 = (int) encryptedText.size() - out_len1; rc = EVP_EncryptFinal(ctx.get(), (unsigned char *) &encryptedText[0] + out_len1, &out_len2); if (rc != 1) { return -4; } // Set cipher text size now that we know it encryptedText.resize(out_len1 + out_len2); return 0; } catch (std::exception &e) { #if LOG_CRYPTO_ERRORS dbg_println("[crypto] Error while aes encrypting: {}", e.what()); #endif return -99; } }
程序运行结果如下:
Application shutdown Encrypting 73687574646f776e with key 3132333435363738393031323334353637383930313233343536373839303132 and messageIv 3132 3334353637383930313233343536 Encrypting with key 3132333435363738393031323334353637383930313233343536373839303132 and iv 3132333435363738393031323334 3536 and status: not null err: error:00000000:lib(0)::reason(0) [crypto] Error while aes encrypting: 0 Application aes_encrypt failed -2
分析与解决方案
核心问题:OpenSSL库资源已提前销毁
程序退出阶段,全局对象的析构顺序与初始化顺序相反。虽然Application最后创建、最先析构,但OpenSSL的全局内部资源(如算法注册表、错误处理模块)可能已经被其他更早销毁的全局对象,或是程序退出时的系统自动清理流程释放了。此时调用EVP_EncryptInit会因依赖的底层资源不存在而失败,ERR_error_string返回空错误信息也印证了这一点——错误处理模块已不可用。验证方法
可以在aes_encrypt函数开头添加检查:调用EVP_get_cipherbyname("aes-256-cbc"),如果返回nullptr,说明AES算法模块已被卸载。可行解决方案
- 调整操作时机:不在析构函数中执行加密操作,将日志记录逻辑提前到程序正常退出的主动调用阶段(比如在
main函数结束前手动调用shutdown,而非依赖对象析构)。 - 手动管理OpenSSL生命周期:如果必须在退出阶段执行加密,需确保OpenSSL资源在加密完成后再销毁。程序启动时调用
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS, nullptr)初始化,在所有退出逻辑完成后调用OPENSSL_cleanup()手动清理,避免系统自动提前释放资源。 - 调整单例销毁顺序:检查是否有其他全局单例会在Application之前销毁,且负责清理OpenSSL资源。若存在,调整这些单例的销毁顺序,确保OpenSSL资源在Application析构完成后再释放。
内容的提问来源于Stack Exchange,提问作者Kaspek
相关产品推荐
相关产品推荐

