Skip to content

Commit 7eb5b52

Browse files
authored
Merge pull request #122 from corhere/err-remove-thread-state
Free thread-local OpenSSL state on thread exit
2 parents 66bdd79 + 7e2df1a commit 7eb5b52

File tree

7 files changed

+122
-6
lines changed

7 files changed

+122
-6
lines changed

goopenssl.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
#include "shims.h"
66

7+
// Suppress warnings about unused parameters.
8+
#define UNUSED(x) (void)(x)
79

810
static inline void
911
go_openssl_do_leak_check(void)

shims.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ DEFINEFUNC_3_0(unsigned long, ERR_get_error_all, (const char **file, int *line,
172172
DEFINEFUNC_RENAMED_1_1(const char *, OpenSSL_version, SSLeay_version, (int type), (type)) \
173173
DEFINEFUNC(void, OPENSSL_init, (void), ()) \
174174
DEFINEFUNC_LEGACY_1_0(void, ERR_load_crypto_strings, (void), ()) \
175+
DEFINEFUNC_LEGACY_1_0(void, ERR_remove_thread_state, (const GO_CRYPTO_THREADID_PTR tid), (tid)) \
175176
DEFINEFUNC_LEGACY_1_0(int, CRYPTO_num_locks, (void), ()) \
176177
DEFINEFUNC_LEGACY_1_0(int, CRYPTO_THREADID_set_callback, (void (*threadid_func) (GO_CRYPTO_THREADID_PTR)), (threadid_func)) \
177178
DEFINEFUNC_LEGACY_1_0(void, CRYPTO_THREADID_set_numeric, (GO_CRYPTO_THREADID_PTR id, unsigned long val), (id, val)) \

thread_setup.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package openssl
2+
3+
// Go wrappers for testing the thread setup code as _test.go files cannot import "C".
4+
5+
// #include "thread_setup.h"
6+
import "C"
7+
8+
// opensslThreadsCleanedUp returns the number of times the thread-local OpenSSL
9+
// state has been cleaned up since the process started.
10+
func opensslThreadsCleanedUp() uint {
11+
return uint(C.go_openssl_threads_cleaned_up)
12+
}

thread_setup.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#define CRYPTO_LOCK 0x01
2+
3+
/* Used by unit tests. */
4+
extern volatile unsigned int go_openssl_threads_cleaned_up;

thread_setup_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package openssl
2+
3+
import (
4+
"runtime"
5+
"testing"
6+
"time"
7+
)
8+
9+
func init() {
10+
// The runtime parks the "main" thread of the process on Linux rather of
11+
// terminating it. Lock the main thread to the initial goroutine to ensure
12+
// that the test goroutines will always be scheduled onto non-main threads
13+
// that can be consistently made to terminate on demand.
14+
runtime.LockOSThread()
15+
}
16+
17+
func TestThreadCleanup(t *testing.T) {
18+
if vMajor > 1 || vMinor > 0 {
19+
t.Skip("explicit thread cleanup is only needed for OpenSSL 1.0.x")
20+
}
21+
22+
before := opensslThreadsCleanedUp()
23+
done := make(chan struct{})
24+
go func() {
25+
defer close(done)
26+
// The thread this goroutine is running on will be terminated by the
27+
// runtime when the goroutine exits.
28+
runtime.LockOSThread()
29+
// Checking for errors has the side effect of initializing
30+
// the thread-local OpenSSL error queue.
31+
_ = newOpenSSLError("")
32+
}()
33+
<-done
34+
time.Sleep(100 * time.Millisecond) // Give some time for the thread to terminate.
35+
after := opensslThreadsCleanedUp()
36+
37+
if n := after - before; n != 1 {
38+
t.Errorf("expected thread cleanup to have run once, but it ran %d times", n)
39+
}
40+
}

thread_setup_unix.c

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
//go:build unix
22

33
#include "goopenssl.h"
4+
#include "thread_setup.h"
45
#include <pthread.h>
5-
6-
#define CRYPTO_LOCK 0x01
76

87
/* This array will store all of the mutexes available to OpenSSL. */
98
static pthread_mutex_t *mutex_buf = NULL;
10-
9+
10+
static pthread_key_t destructor_key;
11+
12+
/* Used by unit tests. */
13+
volatile unsigned int go_openssl_threads_cleaned_up = 0;
14+
1115
static void locking_function(int mode, int n, const char *file, int line)
1216
{
1317
if (mode & CRYPTO_LOCK)
@@ -19,10 +23,32 @@ static void locking_function(int mode, int n, const char *file, int line)
1923
static void thread_id(GO_CRYPTO_THREADID_PTR tid)
2024
{
2125
go_openssl_CRYPTO_THREADID_set_numeric(tid, (unsigned long)pthread_self());
26+
27+
// OpenSSL fetches the current thread ID whenever it does anything with the
28+
// per-thread error state, so this function is guaranteed to be executed at
29+
// least once on any thread with associated error state. The thread-local
30+
// variable needs to be set to a non-NULL value so that the destructor will
31+
// be called when the thread exits. The actual value does not matter.
32+
(void) pthread_setspecific(destructor_key, (void*)1);
33+
}
34+
35+
static void cleanup_thread_state(void *ignored)
36+
{
37+
UNUSED(ignored);
38+
go_openssl_ERR_remove_thread_state(NULL);
39+
// ERR_remove_thread_state(NULL) in turn calls our registered thread_id
40+
// callback via CRYPTO_THREADID_current(), which sets the thread-local
41+
// variable associated with this destructor to a non-NULL value. We have to
42+
// clear the variable ourselves to prevent pthreads from calling the
43+
// destructor again for the same thread.
44+
(void) pthread_setspecific(destructor_key, NULL);
45+
go_openssl_threads_cleaned_up++;
2246
}
2347

2448
int go_openssl_thread_setup(void)
2549
{
50+
if (pthread_key_create(&destructor_key, cleanup_thread_state) != 0)
51+
return 0;
2652
mutex_buf = malloc(go_openssl_CRYPTO_num_locks()*sizeof(pthread_mutex_t));
2753
if (!mutex_buf)
2854
return 0;

thread_setup_windows.c

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
//go:build windows
22

33
#include "goopenssl.h"
4+
#include "thread_setup.h"
45

56
#include <stdlib.h>
67
#include <windows.h>
78

8-
#define CRYPTO_LOCK 0x01
9-
109
/* This array will store all of the mutexes available to OpenSSL. */
1110
static HANDLE *mutex_buf = NULL;
1211

12+
static DWORD fls_index = FLS_OUT_OF_INDEXES;
13+
14+
/* Used by unit tests. */
15+
volatile unsigned int go_openssl_threads_cleaned_up = 0;
16+
1317
static void locking_function(int mode, int n, const char *file, int line)
1418
{
1519
if (mode & CRYPTO_LOCK)
@@ -18,16 +22,43 @@ static void locking_function(int mode, int n, const char *file, int line)
1822
ReleaseMutex(mutex_buf[n]);
1923
}
2024

25+
static void thread_id(GO_CRYPTO_THREADID_PTR tid)
26+
{
27+
go_openssl_CRYPTO_THREADID_set_numeric(tid, (unsigned long)GetCurrentThreadId());
28+
29+
// OpenSSL fetches the current thread ID whenever it does anything with the
30+
// per-thread error state, so this function is guaranteed to be executed at
31+
// least once on any thread with associated error state. As the Win32 API
32+
// reference documentation is unclear on whether the fiber-local storage
33+
// slot needs to be set to trigger the destructor on thread exit, set it to
34+
// a non-NULL value just in case.
35+
(void) FlsSetValue(fls_index, (void*)1);
36+
go_openssl_threads_cleaned_up++;
37+
}
38+
39+
static void cleanup_thread_state(void *ignored)
40+
{
41+
UNUSED(ignored);
42+
go_openssl_ERR_remove_thread_state(NULL);
43+
}
44+
2145
int go_openssl_thread_setup(void)
2246
{
47+
// Use the fiber-local storage API to hook a callback on thread exit.
48+
// https://devblogs.microsoft.com/oldnewthing/20191011-00/?p=102989
49+
fls_index = FlsAlloc(cleanup_thread_state);
50+
if (fls_index == FLS_OUT_OF_INDEXES)
51+
return 0;
2352
mutex_buf = malloc(go_openssl_CRYPTO_num_locks()*sizeof(HANDLE));
2453
if (!mutex_buf)
2554
return 0;
2655
int i;
2756
for (i = 0; i < go_openssl_CRYPTO_num_locks(); i++)
2857
mutex_buf[i] = CreateMutex(NULL, FALSE, NULL);
2958
go_openssl_CRYPTO_set_locking_callback(locking_function);
30-
// go_openssl_CRYPTO_set_id_callback is not needed on Windows
59+
// go_openssl_CRYPTO_set_id_callback is not strictly needed on Windows
3160
// as OpenSSL uses GetCurrentThreadId() by default.
61+
// But we need to piggyback off the callback for our own purposes.
62+
go_openssl_CRYPTO_THREADID_set_callback(thread_id);
3263
return 1;
3364
}

0 commit comments

Comments
 (0)