Skip to content

Fix memory leak due caused due to X509_STORE #671

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 2, 2020

Conversation

omjego
Copy link
Contributor

@omjego omjego commented Sep 28, 2020

When X509_STORE objects are associated with a SSL_CTX (ctx_), they will automatically be deleted when the parent SSL_CTX is deleted (in the destructor). However, library lazily sets these in load_certs(), only doing it on a valid HTTP request (after the socket connects inside the call to initialize_ssl()). It causes a memory leak when connecting to an invalid URL, since it can't establish the socket.
The current PR has quick fix for the issue, although not sure if this is the ideal solution. Should we associate the X509_STORE with the ctx_t immediately in set_ca_cert_store() ?

@omjego omjego closed this Sep 28, 2020
@omjego omjego reopened this Sep 28, 2020
@yhirose
Copy link
Owner

yhirose commented Sep 29, 2020

@omjego, thanks for the pull request. How did you detect memory leak? Can I reproduce it easily on my Mac? Also I wonder if you can add a unit test to test/test.cc.

@omjego
Copy link
Contributor Author

omjego commented Sep 29, 2020

Hey @yhirose the memory leak was detected with address sanitiser. Have added a test for repro and address sanitizer flag to the test Makefile which will be useful for finding such bugs in future.

@yhirose
Copy link
Owner

yhirose commented Sep 29, 2020

@omjego, thanks for adding the unit test. I now fully understand the situation. I reviewed the pull request, but it seems to be not enough. Here is an example:

{
  httplib::SSLClient cli("www.google.com");

  auto cs1 = X509_STORE_new();
  X509_STORE_load_locations(cs1, "/etc/ssl/certs/ca-certificates.crt", nullptr);
  cli.set_ca_cert_store(cs1);

  auto cs2 = X509_STORE_new();
  X509_STORE_load_locations(cs2, "/etc/ssl/certs/ca-certificates.crt", nullptr);
  cli.set_ca_cert_store(cs2);

  // When the destructor of `cli` gets executed, only `cs2` will be freed...
}

So, I am thinking to remove ca_cert_store_ member from SSLClient, and modify set_ca_cert_store and load_certs accordingly:

inline void SSLClient::set_ca_cert_store(X509_STORE *ca_cert_store) {
  if (ca_cert_store) {
    if (ctx_) {
      if (SSL_CTX_get_cert_store(ctx_) != ca_cert_store) {
        // The previous cert store pointer will be freed, and the current one
        // wiil be freed in `~SSLClient()`.
        SSL_CTX_set_cert_store(ctx_, ca_cert_store);
      }
    } else {
      X509_STORE_free(ca_cert_store);
    }
  }
}

inline bool SSLClient::load_certs() {
  bool ret = true;

  std::call_once(initialize_cert_, [&]() {
    std::lock_guard<std::mutex> guard(ctx_mutex_);
    if (!ca_cert_file_path_.empty()) {
      if (!SSL_CTX_load_verify_locations(ctx_, ca_cert_file_path_.c_str(),
                                         nullptr)) {
        ret = false;
      }
    } else if (!ca_cert_dir_path_.empty()) {
      if (!SSL_CTX_load_verify_locations(ctx_, nullptr,
                                         ca_cert_dir_path_.c_str())) {
        ret = false;
      }
    } else {
#ifdef _WIN32
      detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_));
#else
      SSL_CTX_set_default_verify_paths(ctx_);
#endif
    }
  });

  return ret;
}

What do you think?

@omjego
Copy link
Contributor Author

omjego commented Sep 30, 2020

Hey @yhirose, thanks for the example, I kind of missed that. This approach sounds good to me!

@yhirose
Copy link
Owner

yhirose commented Sep 30, 2020

@omjego, I just added the unit test and tested it with -fsanitize=address, but I didn't get any error.
What kind of errors did you get?

TEST(SSLClientTest, UpdateCAStore) {
  httplib::SSLClient httplib_client("www.google.com");
  auto ca_store_1 = X509_STORE_new();
  X509_STORE_load_locations(ca_store_1, "/etc/ssl/certs/ca-certificates.crt",
                            nullptr);
  httplib_client.set_ca_cert_store(ca_store_1);

  auto ca_store_2 = X509_STORE_new();
  X509_STORE_load_locations(ca_store_2, "/etc/ssl/certs/ca-certificates.crt",
                            nullptr);
  httplib_client.set_ca_cert_store(ca_store_2);
}
CXXFLAGS = -ggdb -O0 -std=c++11 -DGTEST_USE_OWN_TR1_TUPLE -I.. -I. -Wall -Wextra -Wtype-limits -Wconversion -fsanitize=address

@omjego
Copy link
Contributor Author

omjego commented Oct 1, 2020

@yhirose
I added the test SSLClientTest.UpdateCAStore to current master and built the binary with -fsanitize=address

./test --gtest_filter=SSLClient*
Following are the INFO and ERROR logs that I got.

[ RUN ] SSLClientTest.UpdateCAStore
[ OK ] SSLClientTest.UpdateCAStore (32 ms)
.
.
.

[ OK ] SSLClientServerTest.TrustDirOptional (14 ms)
[----------] 4 tests from SSLClientServerTest (52 ms total)

[----------] Global test environment tear-down
[==========] 10 tests from 2 test cases ran. (1995 ms total)
[ PASSED ] 10 tests.

==1926963==ERROR: LeakSanitizer: detected memory leaks

Indirect leak of 589008 byte(s) in 12692 object(s) allocated from:
#0 0x7f3a3f3cf628 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x107628)
#1 0x7f3a3f0d9889 in CRYPTO_zalloc (/lib/x86_64-linux-gnu/libcrypto.so.1.1+0x18f889)

Indirect leak of 198314 byte(s) in 260 object(s) allocated from:
#0 0x7f3a3f3cf628 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x107628)
#1 0x7f3a3efffdc9 (/lib/x86_64-linux-gnu/libcrypto.so.1.1+0xb5dc9)

Indirect leak of 175438 byte(s) in 520 object(s) allocated from:
#0 0x7f3a3f3cf628 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x107628)
#1 0x7f3a3efea418 (/lib/x86_64-linux-gnu/libcrypto.so.1.1+0xa0418)

#0 0x7f3a3f3cf628 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x107628)
#1 0x7f3a3f02c05b in BUF_MEM_grow (/lib/x86_64-linux-gnu/libcrypto.so.1.1+0xe205b)

Indirect leak of 65292 byte(s) in 3928 object(s) allocated from:
#0 0x7f3a3f3cf628 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x107628)
#1 0x7f3a3eff3ad7 in ASN1_STRING_set (/lib/x86_64-linux-gnu/libcrypto.so.1.1+0xa9ad7)

Indirect leak of 53466 byte(s) in 520 object(s) allocated from:
#0 0x7f3a3f3cf628 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x107628)
#1 0x7f3a3f15a10b (/lib/x86_64-linux-gnu/libcrypto.so.1.1+0x21010b)

Indirect leak of 11328 byte(s) in 708 object(s) allocated from:
#0 0x7f3a3f3cf628 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x107628)
#1 0x7f3a3effe43d (/lib/x86_64-linux-gnu/libcrypto.so.1.1+0xb443d)
#2 0x94930af9f37c4eff ()

Indirect leak of 10368 byte(s) in 168 object(s) allocated from:
#0 0x7f3a3f3cfa2e in __interceptor_realloc (/lib/x86_64-linux-gnu/libasan.so.5+0x107a2e)
#1 0x7f3a3f13b504 (/lib/x86_64-linux-gnu/libcrypto.so.1.1+0x1f1504)

Indirect leak of 7488 byte(s) in 72 object(s) allocated from:
#0 0x7f3a3f3cf628 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x107628)
#1 0x7f3a3f01c817 in BN_MONT_CTX_new (/lib/x86_64-linux-gnu/libcrypto.so.1.1+0xd2817)

Indirect leak of 720 byte(s) in 36 object(s) allocated from:
#0 0x7f3a3f3cf628 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x107628)
#1 0x7f3a3f069741 in EC_GROUP_copy (/lib/x86_64-linux-gnu/libcrypto.so.1.1+0x11f741)

Indirect leak of 640 byte(s) in 6 object(s) allocated from:
#0 0x7f3a3f3cf628 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x107628)
#1 0x7f3a3f0d9889 in CRYPTO_zalloc (/lib/x86_64-linux-gnu/libcrypto.so.1.1+0x18f889)
#2 0x556023e4818a in void testing::internal::HandleSehExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::)(), char const) gtest/gtest-all.cc:3402
#3 0x556023e3d47d in void testing::internal::HandleExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::)(), char const) gtest/gtest-all.cc:3438
#4 0x556023e13b87 in testing::Test::Run() gtest/gtest-all.cc:3473
#5 0x556023e14a59 in testing::TestInfo::Run() gtest/gtest-all.cc:3650
#6 0x556023e1544e in testing::TestCase::Run() gtest/gtest-all.cc:3757
#7 0x556023e1fcde in testing::internal::UnitTestImpl::RunAllTests() gtest/gtest-all.cc:5549
#8 0x556023e4b451 in bool testing::internal::HandleSehExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>(testing::internal::UnitTestImpl*, bool (testing::internal::UnitTestImpl::)(), char const) gtest/gtest-all.cc:3402
#9 0x556023e3f701 in bool testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>(testing::internal::UnitTestImpl*, bool (testing::internal::UnitTestImpl::)(), char const) gtest/gtest-all.cc:3438
#10 0x556023e1d589 in testing::UnitTest::Run() gtest/gtest-all.cc:5183
#11 0x556023e61385 in main gtest/gtest_main.cc:38
#12 0x7f3a3eae3cc9 in __libc_start_main ../csu/libc-start.c:308

Indirect leak of 214 byte(s) in 24 object(s) allocated from:
#0 0x7f3a3f3cf628 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x107628)
#1 0x7f3a3efed91a (/lib/x86_64-linux-gnu/libcrypto.so.1.1+0xa391a)

UMMARY: AddressSanitizer: 1186412 byte(s) leaked in 19454 allocation(s).

@yhirose
Copy link
Owner

yhirose commented Oct 1, 2020

@omjego, thanks for the info. It seems like clang does't work with -fsanitize=address on my Mac. It doesn't give the memory leak report on the SSLClientTest.UpdateCAStore...

@yhirose
Copy link
Owner

yhirose commented Oct 1, 2020

@omjego, could you also fix the conflicts in your pull request? Then, I'll merge it to the master. Thanks!

@omjego
Copy link
Contributor Author

omjego commented Oct 2, 2020

@yhirose oh I'm using linux, seems address sanitizer isn't supported on mac. Should I remove the sanitizer option from the makefile or make it conditional based on os?

@yhirose
Copy link
Owner

yhirose commented Oct 2, 2020

@omjego, I am ok with the current Makefile, since it doesn't cause any side effect. Thanks for your contribution!

@yhirose yhirose merged commit 143b2dd into yhirose:master Oct 2, 2020
@omjego omjego deleted the x509-mem-leak-fix branch October 7, 2020 11:41
ExclusiveOrange pushed a commit to ExclusiveOrange/cpp-httplib-exor that referenced this pull request May 2, 2023
* Fix memory leak due caused due to X509_STORE

* Add test for repro and address sanitizer to compiler flags

* Add comment

* Sync

* Associate ca_store with ssl context within set_ca_cert_store()

* Split SlowPost test

* Fix yhirose#674

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants