sitelogo

By

Синхронизация OpenSSL в многопоточных приложениях

Сегодня на работе пофиксил багу, связанную с зависанием при выходе из приложения. Реализация класса TLS сокетов иллюстрировала попытки разработчика впихнуть невпихуемое, но показать этот код я к сожалению не могу. Если кратко, были три основных метода Receive, Send и Close, работавшие из разных потоков. В каждом методе было по мьютексу, так как класс еще и обязан был поддерживать блокирующие и неблокирующие сокеты. Если бы использовался один мьютекс на весь класс, тогда блокирующий SSL_read мог бы заблочить все остальные потоки, когда те попытались бы записать данные или закрыть сокет.

Нужно было поведение, при котором вызов Close прерывал бы блокирующую операцию чтения или записи. Если использовать несвязанные мьютексы в каждом методе, этого добиться можно, но тогда возникает ситуация, когда SSL_read и SSL_write будут вызваны одновременно. Можно использовать TryLock, но избежать одновременного вызова всё равно не получится, а завершать SSL соединения придётся, «обрубая концы», вызывая close() на TCP сокете.

Официальная документация говорит о недопустимости одновременных вызовов библиотеки в случае отсутствия поддержки многопоточности, но наш код был написан криво, а потому работал почти стабильно. Либо в те времена библиотека вообще не поддерживала многопоточность, либо проектировщик и правда не знал про документацию. Надо отдать должное, ведь всё это работало как минимум лет пять.

Давайте представим ситуацию, когда одновременно будут вызваны read и write. Для получения пакета данных из сети (чтения), SSL может выполнять отправку служебной информации в сеть, меняя состояние объекта SSL. Если мы в другом потоке начнем отправлять данные, используя тот же объект SSL и сокет, то информация о состоянии в нём попросту перепутается. Таким образом, поставленная задача невыполнима без вовлечения нижележащей библиотеки в синхронизацию своих данных. А синхронизация слишком больших кусков данных на высоком уровне довольно сильно ограничивает возможности программы для маневра.

Для того, чтобы OpenSSL стала потокобезопасной, нужно установить для неё функции обратного вызова. Для их описания идите и читайте документацию. А я думаю, было бы интереснее посмотреть, как это делается на практике под конкретную платформу. В интернете я только нашёл примеры для POSIX, после чего портировал их под Windows. Листинги вы видите ниже.

POSIX, C:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
struct CRYPTO_dynlock_value
{
        pthread_mutex_t mutex;
};
 
static struct CRYPTO_dynlock_value *static_locks = NULL;
 
static struct CRYPTO_dynlock_value *dyn_create_function(const char *file, int line)
{
        struct CRYPTO_dynlock_value *value;
 
        value = (struct CRYPTO_dynlock_value *)malloc(sizeof(*value));
        if (!value)
                return NULL;
 
        assert(pthread_mutex_init(&value->mutex, NULL) == 0);
        return value;
}
 
static void dyn_lock_function(int mode, struct CRYPTO_dynlock_value *value,
        const char *file, int line)
{
        if (mode & CRYPTO_LOCK)
                assert(pthread_mutex_lock(&value->mutex) == 0);
        else
                assert(pthread_mutex_unlock(&value->mutex) == 0);
}
 
static void dyn_destroy_function(struct CRYPTO_dynlock_value *value,
        const char *file, int line)
{
        assert(pthread_mutex_destroy(&value->mutex) == 0);
        free(value);
}
 
static void locking_function(int mode, int index, const char *file, int line)
{
        dyn_lock_function(mode, &static_locks[index], file, line);
}
 
static unsigned long id_function(void)
{
        /* Not conforming to POSIX and not guaranteed to work, but from real application... */
        return ((unsigned long)pthread_self());
}
 
void openssl_threads_setup()
{
        int i, numlocks;
 
        numlocks = CRYPTO_num_locks();
        static_locks = (struct CRYPTO_dynlock_value *)malloc(numlocks * sizeof(*static_locks));
        assert(static_locks);
        for (i = 0; i < numlocks; i++)
                assert(pthread_mutex_init(&static_locks[i].mutex, NULL) == 0);
 
        CRYPTO_set_id_callback(id_function);
        CRYPTO_set_locking_callback(locking_function);
        CRYPTO_set_dynlock_create_callback(dyn_create_function);
        CRYPTO_set_dynlock_destroy_callback(dyn_destroy_function);
        CRYPTO_set_dynlock_lock_callback(dyn_lock_function);
}
 
void openssl_threads_cleanup()
{
        int i;
 
        CRYPTO_set_id_callback(NULL);
        CRYPTO_set_locking_callback(NULL);
        CRYPTO_set_dynlock_create_callback(NULL);
        CRYPTO_set_dynlock_destroy_callback(NULL);
        CRYPTO_set_dynlock_lock_callback(NULL);
 
        for (i = 0; i < CRYPTO_num_locks(); i++)
                assert(pthread_mutex_destroy(&static_locks[i].mutex) == 0);
 
        free(static_locks);
        static_locks = NULL;
}

Соответственно, заголовочный файл будет иметь всего 2 функции — openssl_threads_setup и openssl_threads_cleanup. Пользоваться этим достаточно просто — нужно просто вызвать первую функцию в начале main программы, а вторую — в конце. Код выше я смастерил, не компилируя, так что возможны ошибки.

Теперь рассмотрим пример на C++ для Windoze. Так как этот язык у нас самый продвинутый, приходится всё усложнять. А именно, в больших проектах не всегда желательно лезть в main, а лучше проинициализировать коллбеки «по запросу», обращаясь к синглтону (COpenSSLThreads& thr = COpenSSLThreads::Instance()).

Windows, C++ header:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef OPENSSLTHREADS_H_INCL__
#define OPENSSLTHREADS_H_INCL__
 
class COpenSSLThreads
{
public:
        COpenSSLThreads();
        virtual ~COpenSSLThreads();
        static COpenSSLThreads &Instance();
protected:
        static struct CRYPTO_dynlock_value *sslDynCreateCallback(const char *pcFile, int nLine);
        static void sslDynLockCallback(int nMode, struct CRYPTO_dynlock_value *pValue, const char *pcFile, int nLine);
        static void sslDynDestroyCallback(struct CRYPTO_dynlock_value *pValue, const char *pcFile, int nLine);
        static void sslLockingCallback(int nMode, int nIndex, const char *pcFile, int nLine);
        static unsigned long sslIdCallback(void);
protected:
        static CList<struct CRYPTO_dynlock_value *> *m_pLocks;
};
 
#endif

Windows, C++ code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include "OpenSSLThreads.h"
 
CList<struct CRYPTO_dynlock_value *> *COpenSSLThreads::m_pLocks = NULL;
 
COpenSSLThreads::COpenSSLThreads()
{
        int numlocks = CRYPTO_num_locks();
        m_pLocks = new CList<struct CRYPTO_dynlock_value *>(numlocks);
        for (int i = 0; i < numlocks; i++)
                m_pLocks[i] = (struct CRYPTO_dynlock_value *)(void *)new CSyncLock();
 
        CRYPTO_set_id_callback(sslIdCallback);
        CRYPTO_set_locking_callback(sslLockingCallback);
        CRYPTO_set_dynlock_create_callback(sslDynCreateCallback);
        CRYPTO_set_dynlock_destroy_callback(sslDynDestroyCallback);
        CRYPTO_set_dynlock_lock_callback(sslDynLockCallback);
}
 
COpenSSLThreads::~COpenSSLThreads()
{
        CRYPTO_set_id_callback(NULL);
        CRYPTO_set_locking_callback(NULL);
        CRYPTO_set_dynlock_create_callback(NULL);
        CRYPTO_set_dynlock_destroy_callback(NULL);
        CRYPTO_set_dynlock_lock_callback(NULL);
 
        for (int i = 0; i < m_pLocks->GetSize(); i++)
                delete (CSyncLock *)(void *)(*m_pLocks)[i];
 
        delete m_pLocks;
        m_pLocks = NULL;
}
 
COpenSSLThreads &COpenSSLThreads::Instance()
{
        CSyncLock initLock();
        CSyncAutoLock initAutoLock(initLock);
        static COpenSSLThreads instance;
 
        return instance;
}
 
struct CRYPTO_dynlock_value *sslDynCreateCallback(const char *pcFile, int nLine)
{
        LOGDBG(TXT("%S: Enter (%S:%d)\n"), FUNC_NAME, pcFile, nLine);
 
        return (struct CRYPTO_dynlock_value *)(void *)new CSyncLock();
}
 
void sslDynLockCallback(int nMode, struct CRYPTO_dynlock_value *pValue, const char *pcFile, int nLine)
{
        CSyncLock lock = (CSyncLock *)(void *)pValue;
 
        if (nMode & CRYPTO_LOCK)
                lock->Lock();
        else
                lock->Unlock();
}
 
void sslDynDestroyCallback(struct CRYPTO_dynlock_value *pValue, const char *pcFile, int nLine)
{
        LOGDBG(TXT("%S: Enter (%S:%d)\n"), FUNC_NAME, pcFile, nLine);
 
        delete (CSyncLock *)(void *)pValue;
}
 
void sslLockingCallback(int nMode, int nIndex, const char *pcFile, int nLine)
{
        sslDynLockCallback(nMode, (*m_pLocks)[i], pcFile, nLine);
}
 
unsigned long sslIdCallback(void)
{
        return (unsigned long)GetCurrentThreadId();
}

Код выше я тоже написал по памяти. Очень похоже на то, что работает теперь у нас в проекте. CSyncLock — это кроссплатформенная реализация мьютексов, под винду использует критические секции. CSyncAutoLock — просто лочит секцию, пока объект не разрушится на стеке. CList — кастомная реализация списков данных. Под C++ переопределить CRYPTO_dynlock_value у меня не получилось, но это не важно, в этот тип можно скастовать указатель на любой класс, OpenSSL это переваривает без проблем.

В заключение. С вышеописанной реализацией можно вызывать SSL_read(), SSL_write() и SSL_shutdown() одновременно без опасений закрешить программу. Сокеты могут быть блокирующими и неблокирующими. Если в отдельном потоке вызвать SSL_shutdown() или просто закрыть сокет, остальные потоки прерываются. Функции при этом вызывают ошибки вроде SSL_ERROR_SYSCALL, но ERR_get_error() говорит, что это просто пришёл EOF на сокете, то есть с этим можно жить. Чтобы всё было совсем идеально, можно использовать ASIO (WSARecv + OVERLAPPED.hEvent + WaitForMultipleObjects\CancelIoEx) — проверено, тоже отлично работает.



Добавить комментарий

Ваш e-mail не будет опубликован.