From 5ce75dbf5812fd584c1ce3a03f1a3aa38caeefcc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Deuc=D0=B5?= <shurd@sasktel.net>
Date: Mon, 18 Dec 2023 23:45:31 -0500
Subject: [PATCH] Only mutex proect tls_certificate usage.

Holding the lock around session establishment should not be needed,
but we need to protect tls_certificate read and usage.  Since we
don't have rwlocks in xpdev (yet?), hack together a crappy rwlock
that does what we need.
---
 src/sbbs3/ftpsrvr.c   |  4 +--
 src/sbbs3/js_socket.c |  6 ++--
 src/sbbs3/load_cfg.c  |  6 +++-
 src/sbbs3/mailsrvr.c  | 20 ++++----------
 src/sbbs3/services.c  |  5 ++--
 src/sbbs3/ssl.c       | 64 ++++++++++++++++++++++++++++++++++++++++---
 src/sbbs3/websrvr.c   |  5 ++--
 7 files changed, 79 insertions(+), 31 deletions(-)

diff --git a/src/sbbs3/ftpsrvr.c b/src/sbbs3/ftpsrvr.c
index 6ed419b16d..d1717a6017 100644
--- a/src/sbbs3/ftpsrvr.c
+++ b/src/sbbs3/ftpsrvr.c
@@ -1184,12 +1184,12 @@ static BOOL start_tls(SOCKET *sock, CRYPT_SESSION *sess, BOOL resp)
 			sockprintf(*sock, *sess, "431 TLS not available");
 		return FALSE;
 	}
+	unlock_ssl_cert();
 	nodelay = TRUE;
 	(void)setsockopt(*sock,IPPROTO_TCP,TCP_NODELAY,(char*)&nodelay,sizeof(nodelay));
 	nb=0;
 	ioctlsocket(*sock,FIONBIO,&nb);
 	if ((status = cryptSetAttribute(*sess, CRYPT_SESSINFO_NETWORKSOCKET, *sock)) != CRYPT_OK) {
-		unlock_ssl_cert();
 		GCES(status, *sock, *sess, estr, "setting network socket");
 		cryptDestroySession(*sess);
 		*sess = -1;
@@ -1200,11 +1200,9 @@ static BOOL start_tls(SOCKET *sock, CRYPT_SESSION *sess, BOOL resp)
 	if (resp)
 		sockprintf(*sock, -1, "234 Ready to start TLS");
 	if ((status = cryptSetAttribute(*sess, CRYPT_SESSINFO_ACTIVE, 1)) != CRYPT_OK) {
-		unlock_ssl_cert();
 		GCES(status, *sock, *sess, estr, "setting session active");
 		return TRUE;
 	}
-	unlock_ssl_cert();
 	if (startup->max_inactivity) {
 		if ((status = cryptSetAttribute(*sess, CRYPT_OPTION_NET_READTIMEOUT, startup->max_inactivity)) != CRYPT_OK) {
 			GCES(status, *sock, *sess, estr, "setting read timeout");
diff --git a/src/sbbs3/js_socket.c b/src/sbbs3/js_socket.c
index 96da84eaef..ae2a66dbdf 100644
--- a/src/sbbs3/js_socket.c
+++ b/src/sbbs3/js_socket.c
@@ -2368,24 +2368,22 @@ static JSBool js_socket_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict
 												free_crypt_attrstr(estr);
 											}
 										}
+										lock_ssl_cert();
 										if (scfg->tls_certificate == -1)
 											ret = CRYPT_ERROR_NOTAVAIL;
 										else {
-											lock_ssl_cert();
 											ret = cryptSetAttribute(p->session, CRYPT_SESSINFO_PRIVATEKEY, scfg->tls_certificate);
 											if (ret != CRYPT_OK) {
-												unlock_ssl_cert();
 												GCES(ret, p, estr, "setting private key");
 											}
 										}
+										unlock_ssl_cert();
 									}
 								}
 								if(ret==CRYPT_OK) {
 									if((ret=do_cryptAttribute(p->session, CRYPT_SESSINFO_ACTIVE, 1))!=CRYPT_OK) {
 										GCES(ret, p, estr, "setting session active");
 									}
-									if (tiny == SOCK_PROP_SSL_SERVER)
-										unlock_ssl_cert();
 								}
 							}
 						}
diff --git a/src/sbbs3/load_cfg.c b/src/sbbs3/load_cfg.c
index 4789346f37..f2274fade3 100644
--- a/src/sbbs3/load_cfg.c
+++ b/src/sbbs3/load_cfg.c
@@ -349,8 +349,12 @@ void prep_cfg(scfg_t* cfg)
 void free_cfg(scfg_t* cfg)
 {
 #ifdef USE_CRYPTLIB
-	if (cfg->tls_certificate != -1 && cfg->prepped)
+	lock_ssl_cert();
+	if (cfg->tls_certificate != -1 && cfg->prepped) {
 		cryptDestroyContext(cfg->tls_certificate);
+		cfg->tls_certificate == -1;
+	}
+	unlock_ssl_cert();
 #endif
 	free_node_cfg(cfg);
 	free_main_cfg(cfg);
diff --git a/src/sbbs3/mailsrvr.c b/src/sbbs3/mailsrvr.c
index 9916a7bce3..337783650b 100644
--- a/src/sbbs3/mailsrvr.c
+++ b/src/sbbs3/mailsrvr.c
@@ -1092,21 +1092,19 @@ static bool pop3_client_thread(pop3_t* pop3)
 			GCESH(stat, client.protocol, socket, host_ip, session, "setting private key");
 			return false;
 		}
+		unlock_ssl_cert();
 		nodelay = TRUE;
 		setsockopt(socket,IPPROTO_TCP,TCP_NODELAY,(char*)&nodelay,sizeof(nodelay));
 		nb=0;
 		ioctlsocket(socket,FIONBIO,&nb);
 		if ((stat = cryptSetAttribute(session, CRYPT_SESSINFO_NETWORKSOCKET, socket)) != CRYPT_OK) {
-			unlock_ssl_cert();
 			GCESH(stat, client.protocol, socket, host_ip, session, "setting session socket");
 			return false;
 		}
 		if ((stat = cryptSetAttribute(session, CRYPT_SESSINFO_ACTIVE, 1)) != CRYPT_OK) {
-			unlock_ssl_cert();
 			GCESH(stat, client.protocol, socket, host_ip, session, "setting session active");
 			return false;
 		}
-		unlock_ssl_cert();
 		if (startup->max_inactivity) {
 			if (cryptSetAttribute(session, CRYPT_OPTION_NET_READTIMEOUT, startup->max_inactivity) != CRYPT_OK) {
 				GCESH(stat, client.protocol, socket, host_ip, session, "setting read timeout");
@@ -1216,23 +1214,21 @@ static bool pop3_client_thread(pop3_t* pop3)
 					buf[0] = 0;
 					break;
 				}
+				unlock_ssl_cert();
 				nodelay = TRUE;
 				setsockopt(socket,IPPROTO_TCP,TCP_NODELAY,(char*)&nodelay,sizeof(nodelay));
 				nb=0;
 				ioctlsocket(socket,FIONBIO,&nb);
 				if ((stat = cryptSetAttribute(session, CRYPT_SESSINFO_NETWORKSOCKET, socket)) != CRYPT_OK) {
-					unlock_ssl_cert();
 					GCESH(stat, client.protocol, socket, host_ip, session, "setting network socket");
 					buf[0] = 0;
 					break;
 				}
 				if ((stat=cryptSetAttribute(session, CRYPT_SESSINFO_ACTIVE, 1)) != CRYPT_OK) {
-					unlock_ssl_cert();
 					GCESH(stat, client.protocol, socket, host_ip, session, "setting session active");
 					buf[0] = 0;
 					break;
 				}
-				unlock_ssl_cert();
 				if (startup->max_inactivity) {
 					if ((stat=cryptSetAttribute(session, CRYPT_OPTION_NET_READTIMEOUT, startup->max_inactivity)) != CRYPT_OK) {
 						GCESH(stat, client.protocol, socket, host_ip, session, "setting read timeout");
@@ -2993,21 +2989,19 @@ static bool smtp_client_thread(smtp_t* smtp)
 			GCESH(cstat, client.protocol, socket, host_ip, session, "setting private key");
 			return false;
 		}
+		unlock_ssl_cert();
 		nodelay = TRUE;
 		setsockopt(socket,IPPROTO_TCP,TCP_NODELAY,(char*)&nodelay,sizeof(nodelay));
 		nb=0;
 		ioctlsocket(socket,FIONBIO,&nb);
 		if ((cstat = cryptSetAttribute(session, CRYPT_SESSINFO_NETWORKSOCKET, socket)) != CRYPT_OK) {
-			unlock_ssl_cert();
 			GCESH(cstat, client.protocol, socket, host_ip, session, "setting network socket");
 			return false;
 		}
 		if ((cstat = cryptSetAttribute(session, CRYPT_SESSINFO_ACTIVE, 1)) != CRYPT_OK) {
-			unlock_ssl_cert();
 			GCESH(cstat, client.protocol, socket, host_ip, session, "setting session active");
 			return false;
 		}
-		unlock_ssl_cert();
 		if (startup->max_inactivity) {
 			if ((cstat = cryptSetAttribute(session, CRYPT_OPTION_NET_READTIMEOUT, startup->max_inactivity)) != CRYPT_OK) {
 				GCESH(cstat, client.protocol, socket, host_ip, session, "setting read timeout");
@@ -4975,12 +4969,12 @@ static bool smtp_client_thread(smtp_t* smtp)
 				sockprintf(socket, client.protocol, session, "454 TLS not available");
 				continue;
 			}
+			unlock_ssl_cert();
 			nodelay = TRUE;
 			setsockopt(socket,IPPROTO_TCP,TCP_NODELAY,(char*)&nodelay,sizeof(nodelay));
 			nb=0;
 			ioctlsocket(socket,FIONBIO,&nb);
 			if ((cstat = cryptSetAttribute(session, CRYPT_SESSINFO_NETWORKSOCKET, socket)) != CRYPT_OK) {
-				unlock_ssl_cert();
 				GCESH(cstat, "SMTPS", socket, host_ip, session, "setting network socket");
 				cryptDestroySession(session);
 				session = -1;
@@ -4989,11 +4983,9 @@ static bool smtp_client_thread(smtp_t* smtp)
 			}
 			sockprintf(socket, client.protocol, -1, "220 Ready to start TLS");
 			if ((cstat=cryptSetAttribute(session, CRYPT_SESSINFO_ACTIVE, 1)) != CRYPT_OK) {
-				unlock_ssl_cert();
 				GCESH(cstat, "SMTPS", socket, host_ip, session, "setting session active");
 				break;
 			}
-			unlock_ssl_cert();
 			if (startup->max_inactivity) {
 				if ((cstat=cryptSetAttribute(session, CRYPT_OPTION_NET_READTIMEOUT, startup->max_inactivity)) != CRYPT_OK) {
 					GCESH(cstat, "SMTPS", socket, host_ip, session, "setting read timeout");
@@ -5405,21 +5397,19 @@ static SOCKET sendmail_negotiate(CRYPT_SESSION *session, smb_t *smb, smbmsg_t *m
 							GCESH(status, prot, sock, server, *session, "setting private key");
 							continue;
 						}
+						unlock_ssl_cert();
 						nodelay = TRUE;
 						setsockopt(sock,IPPROTO_TCP,TCP_NODELAY,(char*)&nodelay,sizeof(nodelay));
 						nb=0;
 						ioctlsocket(sock,FIONBIO,&nb);
 						if ((status=cryptSetAttribute(*session, CRYPT_SESSINFO_NETWORKSOCKET, sock)) != CRYPT_OK) {
-							unlock_ssl_cert();
 							GCESH(status, prot, sock, server, *session, "setting network socket");
 							continue;
 						}
 						if ((status=cryptSetAttribute(*session, CRYPT_SESSINFO_ACTIVE, 1)) != CRYPT_OK) {
-							unlock_ssl_cert();
 							GCESHL(status, prot, sock, server, LOG_WARNING, *session, "setting session active");
 							continue;
 						}
-						unlock_ssl_cert();
 						if (startup->max_inactivity) {
 							if ((status=cryptSetAttribute(*session, CRYPT_OPTION_NET_READTIMEOUT, startup->max_inactivity)) != CRYPT_OK) {
 								GCESH(status, prot, sock, server, *session, "setting read timeout");
diff --git a/src/sbbs3/services.c b/src/sbbs3/services.c
index ca70d13500..69efbb9e1c 100644
--- a/src/sbbs3/services.c
+++ b/src/sbbs3/services.c
@@ -1100,18 +1100,17 @@ static void js_service_thread(void* arg)
 		if (scfg.tls_certificate != -1) {
 			HANDLE_CRYPT_CALL(cryptSetAttribute(service_client.tls_sess, CRYPT_SESSINFO_PRIVATEKEY, scfg.tls_certificate), &service_client, "setting private key");
 		}
+		unlock_ssl_cert();
 		BOOL nodelay=TRUE;
 		setsockopt(socket,IPPROTO_TCP,TCP_NODELAY,(char*)&nodelay,sizeof(nodelay));
 
 		HANDLE_CRYPT_CALL(cryptSetAttribute(service_client.tls_sess, CRYPT_SESSINFO_NETWORKSOCKET, socket), &service_client, "setting network socket");
 		if (!HANDLE_CRYPT_CALL(cryptSetAttribute(service_client.tls_sess, CRYPT_SESSINFO_ACTIVE, 1), &service_client, "setting session active")) {
-			unlock_ssl_cert();
 			if (service_client.tls_sess != -1)
 				cryptDestroySession(service_client.tls_sess);
 			js_service_failure_cleanup(service, socket);
 			return;
 		}
-		unlock_ssl_cert();
 	}
 
 #if 0	/* Need to export from SBBS.DLL */
@@ -1999,9 +1998,11 @@ void services_thread(void* arg)
 					lprintf(LOG_ERR, "Option error, TLS not yet supported for static services (%s)", service[i].protocol);
 					continue;
 				}
+				lock_ssl_cert();
 				if(scfg.tls_certificate == -1) {
 					need_cert = TRUE;
 				}
+				unlock_ssl_cert();
 			}
 			service[i].set=xpms_create(startup->bind_retry_count, startup->bind_retry_delay, lprintf);
 			if(service[i].set == NULL) {
diff --git a/src/sbbs3/ssl.c b/src/sbbs3/ssl.c
index 0419ce0abd..66d9095118 100644
--- a/src/sbbs3/ssl.c
+++ b/src/sbbs3/ssl.c
@@ -233,6 +233,7 @@ bool get_crypt_error_string(int status, CRYPT_HANDLE sess, char **estr, const ch
 
 static pthread_once_t crypt_init_once = PTHREAD_ONCE_INIT;
 static pthread_mutex_t ssl_cert_mutex;
+static pthread_mutex_t ssl_rwlock;
 static bool cryptlib_initialized;
 
 static void do_cryptEnd(void)
@@ -253,6 +254,7 @@ static void internal_do_cryptInit(void)
 		lprintf(LOG_ERR,"cryptInit() returned %d", ret);
 	}
 	pthread_mutex_init(&ssl_cert_mutex, NULL);
+	pthread_mutex_init(&ssl_rwlock, NULL);
 	return;
 }
 
@@ -268,14 +270,34 @@ bool is_crypt_initialized(void)
 	return cryptlib_initialized;
 }
 
+static uint32_t readers;
+static uint32_t writers;
+
 void lock_ssl_cert(void)
 {
-	pthread_mutex_lock(&ssl_cert_mutex);
+	pthread_mutex_lock(&ssl_rwlock);
+	if (writers) {
+		pthread_mutex_unlock(&ssl_rwlock);
+		pthread_mutex_lock(&ssl_cert_mutex);
+		readers++;
+		pthread_mutex_unlock(&ssl_cert_mutex);
+	}
+	else {
+		readers++;
+		pthread_mutex_unlock(&ssl_rwlock);
+	}
 }
 
 void unlock_ssl_cert(void)
 {
-	pthread_mutex_unlock(&ssl_cert_mutex);
+	pthread_mutex_lock(&ssl_rwlock);
+	if (readers == 0) {
+		lprintf(LOG_ERR, "Unlocking ssl cert when it's not locked.");
+	}
+	else {
+		readers--;
+	}
+	pthread_mutex_unlock(&ssl_rwlock);
 }
 
 #define DO(action, handle, x)	get_crypt_error_string(x, handle, estr, action, level)
@@ -287,26 +309,48 @@ CRYPT_CONTEXT get_ssl_cert(scfg_t *cfg, char **estr, int *level)
 	CRYPT_CERTIFICATE	ssl_cert;
 	char				sysop_email[sizeof(cfg->sys_inetaddr)+6];
 	char				str[MAX_PATH+1];
+	int done = 0;
 
 	if (estr)
 		*estr = NULL;
 	if(!do_cryptInit())
 		return -1;
+	pthread_mutex_lock(&ssl_rwlock);
+	writers++;
+	// Lock cert mutex while we hold rwlock to ensure we have it whenever writers is non-zero.
 	pthread_mutex_lock(&ssl_cert_mutex);
+	pthread_mutex_unlock(&ssl_rwlock);
+	// Drain all the readers
+	while (!done) {
+		pthread_mutex_lock(&ssl_rwlock);
+		done = (readers == 0);
+		pthread_mutex_unlock(&ssl_rwlock);
+		if (!done) {
+			YIELD();
+		}
+	}
 	SAFEPRINTF2(str,"%s%s",cfg->ctrl_dir,"ssl.cert");
 	time32_t fd = (time32_t)fdate(str);
 	if (cfg->tls_certificate != -1 || !cfg->prepped) {
 		if (fd == cfg->tls_cert_file_date) {
+			ssl_context = cfg->tls_certificate;
+			pthread_mutex_lock(&ssl_rwlock);
+			writers--;
+			pthread_mutex_unlock(&ssl_rwlock);
 			pthread_mutex_unlock(&ssl_cert_mutex);
-			return cfg->tls_certificate;
+			return ssl_context;
 		}
 		cfg->tls_cert_file_date = fd;
 		cryptDestroyContext(cfg->tls_certificate);
+		cfg->tls_certificate = -1;
 	}
 	cfg->tls_cert_file_date = fd;
 	/* Get the certificate... first try loading it from a file... */
 	if(cryptStatusOK(cryptKeysetOpen(&ssl_keyset, CRYPT_UNUSED, CRYPT_KEYSET_FILE, str, CRYPT_KEYOPT_READONLY))) {
 		if(!DO("getting private key", ssl_keyset, cryptGetPrivateKey(ssl_keyset, &ssl_context, CRYPT_KEYID_NAME, "ssl_cert", cfg->sys_pass))) {
+			pthread_mutex_lock(&ssl_rwlock);
+			writers--;
+			pthread_mutex_unlock(&ssl_rwlock);
 			pthread_mutex_unlock(&ssl_cert_mutex);
 			return -1;
 		}
@@ -314,6 +358,9 @@ CRYPT_CONTEXT get_ssl_cert(scfg_t *cfg, char **estr, int *level)
 	else {
 		/* Couldn't do that... create a new context and use the cert from there... */
 		if(!DO("creating SSL context", CRYPT_UNUSED,cryptCreateContext(&ssl_context, CRYPT_UNUSED, CRYPT_ALGO_RSA))) {
+			pthread_mutex_lock(&ssl_rwlock);
+			writers--;
+			pthread_mutex_unlock(&ssl_rwlock);
 			pthread_mutex_unlock(&ssl_cert_mutex);
 			return -1;
 		}
@@ -351,6 +398,8 @@ CRYPT_CONTEXT get_ssl_cert(scfg_t *cfg, char **estr, int *level)
 		cryptDestroyCert(ssl_cert);
 		cryptKeysetClose(ssl_keyset);
 		cryptDestroyContext(ssl_context);
+		cfg->tls_certificate = -1;
+		ssl_context = -1;
 		// Finally, load it from the file.
 		if(cryptStatusOK(cryptKeysetOpen(&ssl_keyset, CRYPT_UNUSED, CRYPT_KEYSET_FILE, str, CRYPT_KEYOPT_READONLY))) {
 			if(!DO("getting private key", ssl_keyset, cryptGetPrivateKey(ssl_keyset, &ssl_context, CRYPT_KEYID_NAME, "ssl_cert", cfg->sys_pass))) {
@@ -360,8 +409,11 @@ CRYPT_CONTEXT get_ssl_cert(scfg_t *cfg, char **estr, int *level)
 	}
 
 	cryptKeysetClose(ssl_keyset);
-	pthread_mutex_unlock(&ssl_cert_mutex);
 	cfg->tls_certificate = ssl_context;
+	pthread_mutex_lock(&ssl_rwlock);
+	writers--;
+	pthread_mutex_unlock(&ssl_rwlock);
+	pthread_mutex_unlock(&ssl_cert_mutex);
 	return ssl_context;
 
 failure_return_3:
@@ -370,6 +422,10 @@ failure_return_2:
 	cryptKeysetClose(ssl_keyset);
 failure_return_1:
 	cryptDestroyContext(ssl_context);
+	cfg->tls_certificate = -1;
+	pthread_mutex_lock(&ssl_rwlock);
+	writers--;
+	pthread_mutex_unlock(&ssl_rwlock);
 	pthread_mutex_unlock(&ssl_cert_mutex);
 	return -1;
 }
diff --git a/src/sbbs3/websrvr.c b/src/sbbs3/websrvr.c
index 2ef210b496..84751316c1 100644
--- a/src/sbbs3/websrvr.c
+++ b/src/sbbs3/websrvr.c
@@ -6613,18 +6613,17 @@ void http_session_thread(void* arg)
 			HANDLE_CRYPT_CALL(cryptSetAttribute(session.tls_sess, CRYPT_SESSINFO_SSL_OPTIONS, CRYPT_SSLOPTION_DISABLE_CERTVERIFY), &session, "disabling certificate verification");
 			HANDLE_CRYPT_CALL(cryptSetAttribute(session.tls_sess, CRYPT_SESSINFO_PRIVATEKEY, scfg.tls_certificate), &session, "setting private key");
 		}
+		unlock_ssl_cert();
 		BOOL nodelay=TRUE;
 		setsockopt(session.socket,IPPROTO_TCP,TCP_NODELAY,(char*)&nodelay,sizeof(nodelay));
 
 		//HANDLE_CRYPT_CALL(cryptSetAttribute(session.tls_sess, CRYPT_SESSINFO_SSL_OPTIONS, CRYPT_SSLOPTION_MINVER_TLS12), &session, "setting TLS minver to 1.2");
 		HANDLE_CRYPT_CALL(cryptSetAttribute(session.tls_sess, CRYPT_SESSINFO_NETWORKSOCKET, session.socket), &session, "setting network socket");
 		if (!HANDLE_CRYPT_CALL(cryptSetAttribute(session.tls_sess, CRYPT_SESSINFO_ACTIVE, 1), &session, "setting session active")) {
-			unlock_ssl_cert();
 			close_session_no_rb(&session);
 			thread_down();
 			return;
 		}
-		unlock_ssl_cert();
 		HANDLE_CRYPT_CALL(cryptSetAttribute(session.tls_sess, CRYPT_OPTION_NET_READTIMEOUT, 0), &session, "setting read timeout");
 	}
 
@@ -7239,10 +7238,12 @@ void web_server(void* arg)
 		 * Add interfaces
 		 */
 		xpms_add_list(ws_set, PF_UNSPEC, SOCK_STREAM, 0, startup->interfaces, startup->port, "Web Server", open_socket, startup->seteuid, NULL);
+		lock_ssl_cert();
 		if(scfg.tls_certificate != -1 && startup->options&WEB_OPT_ALLOW_TLS) {
 			if(do_cryptInit())
 				xpms_add_list(ws_set, PF_UNSPEC, SOCK_STREAM, 0, startup->tls_interfaces, startup->tls_port, "Secure Web Server", open_socket, startup->seteuid, "TLS");
 		}
+		unlock_ssl_cert();
 
 		listInit(&log_list,/* flags */ LINK_LIST_MUTEX|LINK_LIST_SEMAPHORE);
 		if(startup->options&WEB_OPT_HTTP_LOGGING) {
-- 
GitLab