Skip to content

Instantly share code, notes, and snippets.

@rzarzynski
Last active July 12, 2019 03:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rzarzynski/b2e73a4fefd86c1cbbafc76f8e3ee55f to your computer and use it in GitHub Desktop.
Save rzarzynski/b2e73a4fefd86c1cbbafc76f8e3ee55f to your computer and use it in GitHub Desktop.

Investigation of bug #40698

Related changes

The recursive locking for OpenSSL

Kefu points out that MondoDB uses recursive locks:

    // Note: see SERVER-8734 for why we are using a recursive mutex here.
    // Once the deadlock fix in OpenSSL is incorporated into most distros of
    // Linux, this can be changed back to a nonrecursive mutex.
    static std::vector<std::unique_ptr<stdx::recursive_mutex>> _mutex;

Source: ssl_manager_openssl.cpp, line 328-331

The SERVER-8734 ticket is hosted at MondoDB's JIRA.

Analysis

The crash happened in debug build with lockdep available. However, the provided log doesn't contain any backtrace showing where the lock has been initially taken as this information has not been collected.

int lockdep_will_lock(const char *name, int id, bool force_backtrace,
                      bool recursive)
{
  // ...

  lockdep_dout(20) << "_will_lock " << name << " (" << id << ")" << dendl;

  // check dependency graph
  map<int, BackTrace *> &m = held[p];
  for (map<int, BackTrace *>::iterator p = m.begin();
       p != m.end();
       ++p) {
    if (p->first == id) {
      if (!recursive) {
        lockdep_dout(0) << "\n";
        *_dout << "recursive lock of " << name << " (" << id << ")\n";
        BackTrace *bt = new BackTrace(BACKTRACE_SKIP);
        bt->print(*_dout);
        if (p->second) {
          *_dout << "\npreviously locked at\n";
          p->second->print(*_dout);
        }
        delete bt;
        *_dout << dendl;
        ceph_abort();
      }
    }

Likely the reason is lockdep_force_backtrace() returning false during the recording step.

int lockdep_will_lock(const char *name, int id, bool force_backtrace,
                      bool recursive)
{
        // ...
        BackTrace *bt = NULL;
        if (force_backtrace || lockdep_force_backtrace()) {
          bt = new BackTrace(BACKTRACE_SKIP);
        } 
        // ...
}

This can happen when the lockdep_force_backtrace configurable has not been set.

static bool lockdep_force_backtrace()
{
  return (g_lockdep_ceph_ctx != NULL &&
          g_lockdep_ceph_ctx->_conf->lockdep_force_backtrace);
}

CRYPTO_mem_ctrl

Reading through the source code of OpenSSL points out that two mutexes can be involved:

  • CRYPTO_LOCK_MALLOC and
  • the long-standing CRYPTO_LOCK_MALLOC2.

Binary dissection

On Apr 10 2019 the reporter provided a log with the lockdep_force_backtrace turned on. The important part of the log is:

    -2> 2019-07-10T14:38:34.834+0800 7f26c1c7eb80  0 
recursive lock of  (4)
 ceph version v15.0.0-2493-g5d3fd87101 (5d3fd87101e1e45fe372eade4a15a48caed73ba6) octopus (dev)
 1: (ceph::mutex_debug_detail::mutex_debugging_base::_will_lock(bool)+0x4a) [0x7f26b89794ba]
 2: (ceph::shared_mutex_debug::lock()+0x3a) [0x7f26b8a1780c]
 3: (()+0x143a0c0) [0x7f26b88b70c0]
 4: (CRYPTO_mem_ctrl()+0x39) [0x7f26b626dbe9]
 5: (()+0x4a60e) [0x7f26a08f760e]
 6: (SSL_COMP_get_compression_methods()+0x9) [0x7f26a08f9899]
 7: (SSL_library_init()+0x179) [0x7f26a08ff7c9]
 8: (init_hashlib()+0x1f) [0x7f26a0b2214f]
 9: (_PyImport_LoadDynamicModule()+0x99) [0x7f26b71aad69]
 10: (()+0xf7e41) [0x7f26b71a8e41]
 ...

previously locked at
 ceph version v15.0.0-2493-g5d3fd87101 (5d3fd87101e1e45fe372eade4a15a48caed73ba6) octopus (dev)
 1: (ceph::mutex_debug_detail::mutex_debugging_base::_locked()+0x3b) [0x7f26b897950b]
 2: (ceph::shared_mutex_debug::lock()+0xb7) [0x7f26b8a17889]
 3: (()+0x143a0c0) [0x7f26b88b70c0]
 4: (()+0x4a5de) [0x7f26a08f75de]
 5: (SSL_COMP_get_compression_methods()+0x9) [0x7f26a08f9899]
 6: (SSL_library_init()+0x179) [0x7f26a08ff7c9]
 7: (init_hashlib()+0x1f) [0x7f26a0b2214f]
 8: (_PyImport_LoadDynamicModule()+0x99) [0x7f26b71aad69]
 9: (()+0xf7e41) [0x7f26b71a8e41]
 ...
    -1> 2019-07-10T14:38:34.851+0800 7f26c1c7eb80 -1 /home/xuxuehan/mnt/github-ceph/src/common/lockdep.cc: In function 'int lockdep_will_lock(const char*, int, bool, bool)' thread 7f26c1c7eb80 time 2019-07-10T14:38:34.844019+0800
/home/xuxuehan/mnt/github-ceph/src/common/lockdep.cc: 305: ceph_abort_msg("abort() called")

The locker that would have been blocked

6: (SSL_COMP_get_compression_methods()+0x9) [0x7f26a08f9899]:

000000000004c890 <SSL_COMP_get_compression_methods@@libssl.so.10>:
   4c890:       48 83 ec 08             sub    $0x8,%rsp
   4c894:       e8 c7 dc ff ff          callq  4a560 <PEM_write_SSL_SESSION@@libssl.so.10+0x100>
   4c899:       48 8b 05 d0 49 22 00    mov    0x2249d0(%rip),%rax        # 271270 <_edata@@libssl.so.10+0x70>

5: (()+0x4a60e) [0x7f26a08f760e]:

   4a604:       bf 03 00 00 00          mov    $0x3,%edi
   4a609:       e8 e2 01 fd ff          callq  1a7f0 <CRYPTO_mem_ctrl@plt>
   4a60e:       48 8d 3d 9b fe ff ff    lea    -0x165(%rip),%rdi        # 4a4b0 <PEM_write_SSL_SESSION@@libssl.so.10+0x50>

4: (CRYPTO_mem_ctrl()+0x39) [0x7f26b626dbe9]:

The symbol is at offset 0x6ebb0 in /lib64/libcrypto.so.1.0.2k. After adding 0x39 we get 0x6ebe9.

000000000006ebb0 <CRYPTO_mem_ctrl@@libcrypto.so.10>:
   6ebb0:       55                      push   %rbp
   6ebb1:       48 8d 15 fa 65 13 00    lea    0x1365fa(%rip),%rdx        # 1a51b2 <_fini@@libcrypto.so.10+0x5da>
   6ebb8:       b9 d4 00 00 00          mov    $0xd4,%ecx
   6ebbd:       be 14 00 00 00          mov    $0x14,%esi
   6ebc2:       53                      push   %rbx
   6ebc3:       89 fb                   mov    %edi,%ebx
   6ebc5:       bf 09 00 00 00          mov    $0x9,%edi
   6ebca:       48 83 ec 28             sub    $0x28,%rsp
   6ebce:       8b 2d 84 d7 3e 00       mov    0x3ed784(%rip),%ebp        # 45c358 <OPENSSL_NONPIC_relocated@@libcrypto.so.10+0xb0>
   6ebd4:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
   6ebdb:       00 00 
   6ebdd:       48 89 44 24 18          mov    %rax,0x18(%rsp)
   6ebe2:       31 c0                   xor    %eax,%eax
   6ebe4:       e8 a7 ed ff ff          callq  6d990 <CRYPTO_lock@@libcrypto.so.10>
   6ebe9:       83 fb 01                cmp    $0x1,%ebx

The signature of CRYPTO_lock is:

void CRYPTO_lock(int mode, int type, const char *file, int line)

The rigster mapping according to the ABI:

mode -> RDI,
type -> RSI,
file -> RDX,
line -> RCX
# define CRYPTO_LOCK_MALLOC              20

The previous locker

5: (SSL_COMP_get_compression_methods()+0x9) [0x7f26a08f9899]:

000000000004c890 <SSL_COMP_get_compression_methods@@libssl.so.10>:
   4c890:       48 83 ec 08             sub    $0x8,%rsp
   4c894:       e8 c7 dc ff ff          callq  4a560 <PEM_write_SSL_SESSION@@libssl.so.10+0x100>
   4c899:       48 8b 05 d0 49 22 00    mov    0x2249d0(%rip),%rax        # 271270 <_edata@@libssl.so.10+0x70>

4: (()+0x4a5de) [0x7f26a08f75de]:

   4a5c3:       48 8d 15 12 d8 00 00    lea    0xd812(%rip),%rdx        # 57ddc <dtls1_version_str@@libssl.so.10+0x6fc>
   4a5ca:       b9 e0 01 00 00          mov    $0x1e0,%ecx
   4a5cf:       be 10 00 00 00          mov    $0x10,%esi
   4a5d4:       bf 09 00 00 00          mov    $0x9,%edi
   4a5d9:       e8 c2 f2 fc ff          callq  198a0 <CRYPTO_lock@plt>
   4a5de:       48 83 3d 8a 6c 22 00    cmpq   $0x0,0x226c8a(%rip)        # 271270 <_edata@@libssl.so.10+0x70>
# define CRYPTO_LOCK_SSL                 16

No CRYPTO_lock() presence in the backtraces

Compiler has optimized the call to user-provided locking callback.

void CRYPTO_lock(int mode, int type, const char *file, int line)
{
    // ...
    if (type < 0) {
        // ...
    } else if (locking_callback != NULL)
        locking_callback(mode, type, file, line);
}
000000000006d990 <CRYPTO_lock@@libcrypto.so.10>:
   6d990:       41 54                   push   %r12
   6d992:       49 89 d4                mov    %rdx,%r12
   6d995:       55                      push   %rbp
   6d996:       89 fd                   mov    %edi,%ebp
   6d998:       53                      push   %rbx
   6d999:       89 f3                   mov    %esi,%ebx
   6d99b:       48 83 ec 10             sub    $0x10,%rsp
   6d99f:       85 f6                   test   %esi,%esi
   6d9a1:       78 1d                   js     6d9c0 <CRYPTO_lock@@libcrypto.so.10+0x30>
   6d9a3:       48 8b 05 36 e9 3e 00    mov    0x3ee936(%rip),%rax        # 45c2e0 <OPENSSL_NONPIC_relocated@@libcrypto.so.10+0x38>
   6d9aa:       48 85 c0                test   %rax,%rax
   6d9ad:       74 51                   je     6da00 <CRYPTO_lock@@libcrypto.so.10+0x70>
   6d9af:       48 83 c4 10             add    $0x10,%rsp
   6d9b3:       5b                      pop    %rbx
   6d9b4:       5d                      pop    %rbp
   6d9b5:       41 5c                   pop    %r12
   6d9b7:       ff e0                   jmpq   *%rax

Summary

lockdep signalizes recursive locking issue for two independent locks represented by CRYPTO_LOCK_MALLOC and CRYPTO_LOCK_SSL.

Root causes

shared_mutex_debug in conjunction with lockdep name generation relies on the assumption std::string::c_str() always returns different addresses for different instances of the std::string.

void mutex_debugging_base::_register() {
  id = lockdep_register(name.c_str());
}

This is fine for the std::string available on a Ubuntu machine:

rzarzynski@ubulap:/tmp/openssl-pkg$ g++ /tmp/c_str_on_empty_string.cc -o /tmp/c_str_on_empty_string
rzarzynski@ubulap:/tmp/openssl-pkg$ /tmp/c_str_on_empty_string 
a.c_str()=0x7ffdec0a6aa0
b.c_str()=0x7ffdec0a6ac0
rzarzynski@ubulap:/tmp/openssl-pkg$ cat /tmp/c_str_on_empty_string.cc 
#include <string>
#include <iostream>

int main (void) {
  std::string a(std::string{});
  std::string b(std::string{});

  std::cout << "a.c_str()=" << static_cast<const void*>(a.c_str()) << std::endl;
  std::cout << "b.c_str()=" << static_cast<const void*>(b.c_str()) << std::endl;
}

However, on incerta07:

[rzarzynski@incerta07 ~]$ /tmp/test
a.c_str()=0x7f4e13a8de38
b.c_str()=0x7f4e13a8de38
[rzarzynski@incerta07 ~]$ cat /tmp/test.cc
#include <string>
#include <iostream>

int main (void) {
  std::string a(std::string{});
  std::string b(std::string{});

  std::cout << "a.c_str()=" << static_cast<const void*>(a.c_str()) << std::endl;
  std::cout << "b.c_str()=" << static_cast<const void*>(b.c_str()) << std::endl;
}

UPDATE: this was a red herring. lockdep converts const char* backs to std::string. Thefore, the lock_ids map uses comparison operators of std::stirng.

lockdep identifies locks by names, not even by names' addresses

static int _lockdep_register(const char *name)
{
  int id = -1;

  if (!g_lockdep)
    return id;
  ceph::unordered_map<std::string, int>::iterator p = lock_ids.find(name);
  if (p == lock_ids.end()) {
    id = lockdep_get_free_id();
    // ...
  } else {
    id = p->second;
    lockdep_dout(20) << "had '" << name << "' as " << id << dendl;
  }

  ++lock_refs[id];

  return id;
}

While e.g. constructor of shared_mutex_debug is

  // Mutex concept is DefaultConstructible
  shared_mutex_debug()
    : shared_mutex_debug{std::string{}}
  {}

Generality of the problem

From BlueFS:

  struct FileReader {
    // ...
    ceph::shared_mutex lock {
     ceph::make_shared_mutex(std::string(), false, false, false)
    };


    FileReader(FileRef f, uint64_t mpf, bool rand, bool ie)
      : file(f),
        buf(mpf),
        random(rand),
        ignore_eof(ie) {
      ++file->num_readers;
    }
    ~FileReader() {
      --file->num_readers;
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment