使用shared_ptr处理private_key时出现分段错误

51

更新

[X] 我发现当 TLS::credentials creds 在全局范围内声明时,会发生这种情况,但如果我将其声明在外面则不会出现段错误。

我需要它是全局的,因为它有助于缓存证书,并且多个线程可以使用其他线程创建的证书,而不必花费时间创建新证书。

[X] 我进一步将代码从大约200行减少到100行左右

我正在使用Botan创建一个TLS应用程序,但我的应用程序在结束时崩溃并出现段错误。

我尝试使用Valgrind进行调试,但没有任何结果。

以下是来自Valgrind的堆栈跟踪:

==3841967== Invalid write of size 8
==3841967==    at 0x4842964: memset (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==3841967==    by 0x566A82F: Botan::deallocate_memory(void*, unsigned long, unsigned long) (in /usr/lib/x86_64-linux-gnu/libbotan-2.so.12.12.1)
==3841967==    by 0x55E1A4D: ??? (in /usr/lib/x86_64-linux-gnu/libbotan-2.so.12.12.1)
==3841967==    by 0x40EC7B: std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() (shared_ptr_base.h:155)
==3841967==    by 0x40EC29: std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() (shared_ptr_base.h:730)
==3841967==    by 0x41112D: std::__shared_ptr<Botan::RSA_Public_Data const, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr() (shared_ptr_base.h:1169)
==3841967==    by 0x411107: std::shared_ptr<Botan::RSA_Public_Data const>::~shared_ptr() (shared_ptr.h:103)
==3841967==    by 0x41109D: Botan::RSA_PublicKey::~RSA_PublicKey() (rsa.h:25)
==3841967==    by 0x410FC1: Botan::RSA_PrivateKey::~RSA_PrivateKey() (rsa.h:92)
==3841967==    by 0x410DC5: Botan::RSA_PrivateKey::~RSA_PrivateKey() (rsa.h:92)
==3841967==    by 0x410E8A: std::_Sp_counted_ptr<Botan::RSA_PrivateKey*, (__gnu_cxx::_Lock_policy)2>::_M_dispose() (shared_ptr_base.h:377)
==3841967==    by 0x40EC7B: std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() (shared_ptr_base.h:155)
==3841967==  Address 0x9419080 is not stack'd, malloc'd or (recently) free'd
==3841967== 
==3841967== 
==3841967== Process terminating with default action of signal 11 (SIGSEGV)
==3841967==  Access not within mapped region at address 0x9419080
==3841967==    at 0x4842964: memset (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==3841967==    by 0x566A82F: Botan::deallocate_memory(void*, unsigned long, unsigned long) (in /usr/lib/x86_64-linux-gnu/libbotan-2.so.12.12.1)
==3841967==    by 0x55E1A4D: ??? (in /usr/lib/x86_64-linux-gnu/libbotan-2.so.12.12.1)
==3841967==    by 0x40EC7B: std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() (shared_ptr_base.h:155)
==3841967==    by 0x40EC29: std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() (shared_ptr_base.h:730)
==3841967==    by 0x41112D: std::__shared_ptr<Botan::RSA_Public_Data const, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr() (shared_ptr_base.h:1169)
==3841967==    by 0x411107: std::shared_ptr<Botan::RSA_Public_Data const>::~shared_ptr() (shared_ptr.h:103)
==3841967==    by 0x41109D: Botan::RSA_PublicKey::~RSA_PublicKey() (rsa.h:25)
==3841967==    by 0x410FC1: Botan::RSA_PrivateKey::~RSA_PrivateKey() (rsa.h:92)
==3841967==    by 0x410DC5: Botan::RSA_PrivateKey::~RSA_PrivateKey() (rsa.h:92)
==3841967==    by 0x410E8A: std::_Sp_counted_ptr<Botan::RSA_PrivateKey*, (__gnu_cxx::_Lock_policy)2>::_M_dispose() (shared_ptr_base.h:377)
==3841967==    by 0x40EC7B: std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() (shared_ptr_base.h:155)
==3841967==  If you believe this happened as a result of a stack
==3841967==  overflow in your program's main thread (unlikely but
==3841967==  possible), you can try to increase the size of the
==3841967==  main thread stack using the --main-stacksize= flag.
==3841967==  The main thread stack size used in this run was 8388608.
==3841967== 
==3841967== HEAP SUMMARY:
==3841967==     in use at exit: 149,626 bytes in 1,143 blocks
==3841967==   total heap usage: 211,782 allocs, 210,639 frees, 90,582,963 bytes allocated
==3841967== 
==3841967== LEAK SUMMARY:
==3841967==    definitely lost: 0 bytes in 0 blocks
==3841967==    indirectly lost: 0 bytes in 0 blocks
==3841967==      possibly lost: 1,352 bytes in 18 blocks
==3841967==    still reachable: 148,274 bytes in 1,125 blocks
==3841967==                       of which reachable via heuristic:
==3841967==                         newarray           : 1,536 bytes in 16 blocks
==3841967==         suppressed: 0 bytes in 0 blocks
==3841967== Rerun with --leak-check=full to see details of leaked memory
==3841967== 
==3841967== For lists of detected and suppressed errors, rerun with: -s
==3841967== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Segmentation fault (core dumped)

您可以通过以下命令将Botan克隆到您的计算机中:

git clone https://github.com/randombit/botan.git

请按照官方网站的说明进行构建和安装。

您需要在计算机上安装OpenSSL,以创建用于该应用程序的根证书颁发机构。

创建名为testApplication的文件夹并进入其中。

然后使用Bash,输入以下一系列命令来创建根CA:

# Generate private key
openssl genrsa -des3 -out myCA.key 2048
# Generate root certificate
openssl req -x509 -new -nodes -key myCA.key -sha256 -days 825 -out myCA.pem
# Convert to Botan Format
openssl pkcs8 -topk8 -in myCA.key > myCAKey.pkcs8.pem

请使用thisispassword作为密码。

在您的计算机上安装clang编译器,然后可以按照以下方式编译源文件:

clang++ example.cpp -o example  -Wthread-safety -Wall -Wextra -g -std=c++17 -pthread -lssl -lcrypto -lbotan-2 --I/usr/include/botan-2

示例.cpp

#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <sstream>
#include <botan/tls_server.h>
#include <botan/tls_callbacks.h>
#include <botan/tls_session_manager.h>
#include <botan/tls_policy.h>
#include <botan/auto_rng.h>
#include <botan/certstor.h>
#include <botan/pk_keys.h>
#include <botan/pkcs10.h>
#include <botan/pkcs8.h>
#include <botan/x509self.h>
#include <botan/x509path.h>
#include <botan/x509_ca.h>
#include <botan/x509_ext.h>
#include <botan/pk_algs.h>
#include <botan/ber_dec.h>
#include <botan/der_enc.h>
#include <botan/oids.h>
#include <botan/rsa.h>

namespace TLS
{
    typedef std::chrono::duration<int, std::ratio<31556926>> years;

    class credentials : public Botan::Credentials_Manager
    {
    private:
        struct certificate
        {
            std::vector<Botan::X509_Certificate> certs;
            std::shared_ptr<Botan::Private_Key> key;
        };

        std::vector<certificate> creds;
        std::vector<std::shared_ptr<Botan::Certificate_Store>> store;

    public:
        void createCert(std::string hostname)
        {
            /**
             * Initialize Root CA
            **/

            Botan::AutoSeeded_RNG rng;

            const Botan::X509_Certificate rootCert("myCA.pem");

            std::ifstream rootCertPrivateKeyFile("myCAKey.pkcs8.pem");

            Botan::DataSource_Stream rootCertPrivateKeyStream(rootCertPrivateKeyFile);

            std::unique_ptr<Botan::Private_Key> rootCertPrivateKey = Botan::PKCS8::load_key(rootCertPrivateKeyStream, "thisispassword");

            Botan::X509_CA rootCA(rootCert, *rootCertPrivateKey, "SHA-256", rng);

            /**
            * Generate a Cert & Sign with Root CA
            **/

            Botan::X509_Cert_Options opts;
            std::shared_ptr<Botan::Private_Key> serverPrivateKeyShared(new Botan::RSA_PrivateKey(rng, 4096));
            Botan::RSA_PrivateKey* serverPrivateKey = (Botan::RSA_PrivateKey*)serverPrivateKeyShared.get();

            opts.common_name = hostname;
            opts.country = "US";

            auto now = std::chrono::system_clock::now();

            Botan::X509_Time todayDate(now);
            Botan::X509_Time expireDate(now + years(1));

            Botan::PKCS10_Request req = Botan::X509::create_cert_req(opts, *serverPrivateKey, "SHA-256", rng);

            auto serverCert = rootCA.sign_request(req, rng, todayDate, expireDate);

            /**
             * Load Cert to In-Memory Database
            **/

            certificate cert;

            cert.certs.push_back(serverCert);
            cert.key = serverPrivateKeyShared;

            creds.push_back(cert);
        }
    };
}; // namespace TLS

TLS::credentials globalCreds;

int main() {
    globalCreds.createCert("www.google.com");

    std::cout << "End" << "\n";

    return 0;
}

这是来自Botan Lib的函数,Valgrind在此引用:

void deallocate_memory(void* p, size_t elems, size_t elem_size)
   {
   if(p == nullptr)
      return;

   secure_scrub_memory(p, elems * elem_size);

#if defined(BOTAN_HAS_LOCKING_ALLOCATOR)
   if(mlock_allocator::instance().deallocate(p, elems, elem_size))
      return;
#endif

   std::free(p);
   }

用户甚至不需要发送网络流量即可重现问题。我已经包含了一个数据包文件。此外,如果将全局定义的creds对象移动到函数范围内,则问题会消失。 - jeffbRTC
2
这似乎可能是全局对象销毁顺序的问题。移除全局变量globalCreds并将其隐藏在一个“getter”函数中(TLS::credentials &globalCreds() { static TLS::credentials creds; return creds; }),以确保它在需要它的某些库组件被销毁之前被销毁。 - 1201ProgramAlarm
1
@jeffbRTC,你能否在Compiler Explorer上重现这个问题?请问。 - Enlico
1
@Enlico 不行。原因是你必须先构建Botan,然后创建Cert。CE不为我提供shell。 - jeffbRTC
Botan是否有一个"全包含"的头文件呢?如果有的话,在一个简单的示例中可能会有些用处。一些库有,一些没有 - 我没有检查过。 - Kuba hasn't forgotten Monica
@Kuba没有忘记Monica。Botan不是单头文件库,但它可以成为一个(开个拉取请求xD)。 - jeffbRTC
3个回答

41

Botan的作者回复我说,问题出在全局定义的对象。

问题在于全局定义的对象。

问题在于mlock池是一个单例,在第一次使用时创建,然后在main函数返回之后某个时候销毁。首先你的对象被创建。它分配内存。这导致池被创建。析构按照后进先出(LIFO)的顺序进行。因此,首先销毁池。然后销毁你的对象,并尝试访问已经被解除映射的内存(以将其清零)。

解决方案:

  • 创建Botan :: Allocator_Initializer对象以强制初始化在你的对象被创建之前(因此池会一直存在,直到你的对象被销毁之后才会销毁)
  • 禁用locking_allocator模块
  • 将环境变量BOTAN_MLOCK_POOL_SIZE设置为0
  • 不要使用全局变量

原则上,锁定分配器不是通过munmap释放内存,而是将其清零,并留待操作系统在进程退出时解除映射。这可能仍会破坏不变性,但不会像那样严重。它还会导致Valgrind报告泄漏,这很麻烦。

我认为这是因为它直接通过mmap而不是malloc进行映射,所以Valgrind无法跟踪它。


3
问题在于你有一个全局向量,其中包含指向对象的共享指针,因此只有在向量被销毁时才会销毁这些对象;但是该向量的生命周期与池没有关联。你可以在程序退出前显式清除向量,以销毁所指向的对象。 - Peter - Reinstate Monica
1
或者,您可以在与向量相同的翻译单元中创建一个全局“哨兵虚拟”键对象。在同一TU中创建全局对象的顺序是定义的顺序;创建虚拟键会启动池的创建。因此,池将保证在向量之前创建,并因此保证在其之后被销毁。实际上,该虚拟键应该是凭据的成员,并且必须在向量之前声明,始终与(并在)向量一起创建哨兵。 - Peter - Reinstate Monica

12
全局变量,特别是单例,在多线程和复杂应用程序中是一种祸害。这种设计容易引发问题。
我通常的做法是:将所有全局变量在main函数或某个子函数中以正确的顺序定义为局部变量,这样可以在适当的反向顺序下销毁它们。在“几乎所有内容”都依赖于它们的情况下,可以使用类似依赖注入的技术来传递这些对象。在大型、复杂的应用程序(考虑到应用程序本身和数十个库之间的2M行代码)中,我很痛苦地意识到这基本上是唯一可调试的方式。在定制代码和一些有问题的库中清除了全局变量之后,“关闭时的死亡”问题基本上消失了。我不能保证这能解决每个人的问题,因为人们可能会想出新的问题,但我认为这是朝正确方向迈出的一步。

1
我明白了,但是在你的回答中没有找到任何示例。能否分享一个使用依赖注入模拟全局行为的示例? - jeffbRTC
@jeffbRTC 如果必须在程序结束后保持对象的生命周期,那么 DI 本身作为一种模式实际上无法解决问题的根本原因。我认为 Kuba 在这里想表达的更多是通过工厂方法创建对象并管理其内存/生命周期的想法。我认为模式只是一种方便地管理对象生命周期的方式,首先要确定如何保持对象的生命周期以及谁来管理它,不幸的是我没有一个确切的答案。 - jrh
@jeffbRTC ...但是如果我要猜的话,我会说这必须使用操作系统作为中间人与服务通信(例如,在PC上运行的服务,即使您的程序退出后仍然继续运行)。我做过类似的事情,但从未涉及安全性或证书,我猜想这样的东西已经存在于某个地方,因为这似乎是人们普遍遇到的问题。请记住,操作系统也会回收堆内存,因此仅使用new / malloc是不够的。 - jrh
@jrh 不是很想这样做。我不想在程序退出后保持对象的存在。我不需要像systemd那样的东西。我只是想在程序运行时保持它的存在,以便线程可以访问它。 - jeffbRTC
你能分享一个使用依赖注入模拟全局行为的例子吗?这是一个非常大的话题。真的。可以在Google上搜索“C++依赖注入”。一个简单的例子就是将对某个“全局”对象(仅在main中为本地对象)的引用传递给需要它的任何内容。很容易。然后从那个任何内容到其他需要它的任何其他内容,等等。 - Kuba hasn't forgotten Monica

1
这是静态去初始化顺序"fiasco"的示例。
技巧可以防止这种情况,但当链接库时,由于无法控制其生命周期,这些技巧可能不起作用。
因此,在程序退出时明确清除globalCreds的内容可能是最好的解决方案,可以在main函数或atexit函数中实现。如果没有必要进行清理,则下一个最佳解决方案是泄漏结构。
如何泄漏的示例取自isocpp
TLS::credentials& x() {
  static TLS::credentials* creds = new TLS::credentials();
  return *creds;
}
TLS::credentials &globalCreds = x();

我也觉得这会影响我的整洁感。

但这只是一种解决方法,“globalCreds”应该在“main”中创建,并作为引用传递给需要它的类(这些类也在Creds之后在“main”中创建)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接