F-5064: guard logging callback with a mutex and keep native cb registered

pull/388/head
Chris Conlon 2026-07-22 11:03:16 -06:00
parent 4113060968
commit e2cf890b58
3 changed files with 281 additions and 26 deletions

View File

@ -54,6 +54,73 @@ JavaVM* g_vm;
/* global object refs for logging callbacks */
static jobject g_loggingCbIfaceObj;
/* Serializes g_loggingCbIfaceObj reads/updates between NativeLoggingCallback
* and cleanup()/setLoggingCb(). Init in JNI_OnLoad, freed JNI_OnUnload. */
static wolfSSL_Mutex g_loggingCbMutex;
static int g_loggingCbMutexInit = 0;
/* Tracks one-time native registration of NativeLoggingCallback and its return
* value. We register once and never deregister: wolfssl_log reads LogFunction
* without a lock, so clearing it while another thread logs would crash.
* Guarded by g_loggingCbMutex. */
static int g_loggingCbNativeReg = 0;
static int g_loggingCbNativeRet = 0;
static int NativeLoggingCbMutexInit(void)
{
if (g_loggingCbMutexInit) {
return 0;
}
if (wc_InitMutex(&g_loggingCbMutex) != 0) {
return -1;
}
g_loggingCbMutexInit = 1;
return 0;
}
static void NativeLoggingCbMutexFree(void)
{
if (g_loggingCbMutexInit) {
wc_FreeMutex(&g_loggingCbMutex);
g_loggingCbMutexInit = 0;
}
}
static int NativeLoggingCbLock(void)
{
int rc;
if (!g_loggingCbMutexInit) {
return 0;
}
rc = wc_LockMutex(&g_loggingCbMutex);
if (rc != 0) {
WOLFSSL_MSG("Failed to lock logging callback mutex");
}
return rc;
}
static int NativeLoggingCbUnlock(void)
{
int rc;
if (!g_loggingCbMutexInit) {
return 0;
}
rc = wc_UnLockMutex(&g_loggingCbMutex);
if (rc != 0) {
WOLFSSL_MSG("Failed to unlock logging callback mutex");
}
return rc;
}
/* global method IDs we can cache for performance */
jmethodID g_sslIORecvMethodId = NULL;
jmethodID g_sslIORecvMethodId_BB = NULL;
@ -232,6 +299,13 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
return JNI_ERR;
}
/* Initialize the logging callback mutex. */
if (NativeLoggingCbMutexInit() != 0) {
NativeVerifyCbMutexFree();
NativeCrlCbMutexFree();
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
@ -257,6 +331,21 @@ JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved)
/* Free the missing-CRL callback synchronization mutex. */
NativeCrlCbMutexFree();
/* Deregister the native logging callback */
if (g_loggingCbNativeReg) {
wolfSSL_SetLoggingCb(NULL);
g_loggingCbNativeReg = 0;
}
/* Release logging callback global ref. */
if (g_loggingCbIfaceObj != NULL) {
(*env)->DeleteGlobalRef(env, g_loggingCbIfaceObj);
g_loggingCbIfaceObj = NULL;
}
/* Free the logging callback mutex. */
NativeLoggingCbMutexFree();
/* Clear cached method ID */
g_sslIORecvMethodId = NULL;
g_sslIORecvMethodId_BB = NULL;
@ -1629,6 +1718,7 @@ JNIEXPORT jint JNICALL Java_com_wolfssl_WolfSSL_cleanup
(JNIEnv* jenv, jclass jcl)
{
int ret = WOLFSSL_SUCCESS;
jobject loggingCbPrior = NULL;
(void)jenv;
(void)jcl;
@ -1637,9 +1727,13 @@ JNIEXPORT jint JNICALL Java_com_wolfssl_WolfSSL_cleanup
ret = wolfSSL_Cleanup();
/* release global logging callback object if registered */
if (g_loggingCbIfaceObj != NULL) {
(*jenv)->DeleteGlobalRef(jenv, g_loggingCbIfaceObj);
if (NativeLoggingCbLock() == 0) {
loggingCbPrior = g_loggingCbIfaceObj;
g_loggingCbIfaceObj = NULL;
(void)NativeLoggingCbUnlock();
}
if (loggingCbPrior != NULL) {
(*jenv)->DeleteGlobalRef(jenv, loggingCbPrior);
}
#ifdef HAVE_FIPS
@ -1680,6 +1774,8 @@ JNIEXPORT jint JNICALL Java_com_wolfssl_WolfSSL_setLoggingCb
(JNIEnv* jenv, jclass jcl, jobject callback)
{
int ret = 0;
jobject newCbObj = NULL;
jobject priorCbObj = NULL;
(void)jcl;
@ -1687,25 +1783,41 @@ JNIEXPORT jint JNICALL Java_com_wolfssl_WolfSSL_setLoggingCb
return BAD_FUNC_ARG;
}
/* release existing logging callback object if registered */
if (g_loggingCbIfaceObj != NULL) {
(*jenv)->DeleteGlobalRef(jenv, g_loggingCbIfaceObj);
g_loggingCbIfaceObj = NULL;
}
if (callback != NULL) {
/* store Java logging callback Interface object */
g_loggingCbIfaceObj = (*jenv)->NewGlobalRef(jenv, callback);
if (g_loggingCbIfaceObj == NULL) {
newCbObj = (*jenv)->NewGlobalRef(jenv, callback);
if (newCbObj == NULL) {
printf("error storing global logging callback interface\n");
return SSL_FAILURE;
}
ret = wolfSSL_SetLoggingCb(NativeLoggingCallback);
}
else {
/* reset back to null */
ret = wolfSSL_SetLoggingCb(NULL);
/* Swap in new ref and register NativeLoggingCallback once */
if (NativeLoggingCbLock() != 0) {
/* Lock failed, free the new global ref and ret without unlocking */
if (newCbObj != NULL) {
(*jenv)->DeleteGlobalRef(jenv, newCbObj);
}
return SSL_FAILURE;
}
priorCbObj = g_loggingCbIfaceObj;
g_loggingCbIfaceObj = newCbObj;
if (!g_loggingCbNativeReg) {
if (newCbObj != NULL) {
g_loggingCbNativeRet = wolfSSL_SetLoggingCb(NativeLoggingCallback);
g_loggingCbNativeReg = 1;
}
else {
/* Clearing before any registration. */
g_loggingCbNativeRet = wolfSSL_SetLoggingCb(NULL);
}
}
ret = g_loggingCbNativeRet;
(void)NativeLoggingCbUnlock();
/* free prior ref outside the lock */
if (priorCbObj != NULL) {
(*jenv)->DeleteGlobalRef(jenv, priorCbObj);
}
return ret;
@ -1728,6 +1840,7 @@ void NativeLoggingCallback(const int logLevel, const char *const logMessage)
jstring logMsg;
int needsDetach = 0; /* Should we explicitly detach? */
jobjectRefType refcheck;
jobject localCbObj = NULL;
/* get JNIEnv from JavaVM */
vmret = (int)((*g_vm)->GetEnv(g_vm, (void**) &jenv, JNI_VERSION_1_6));
@ -1744,27 +1857,36 @@ void NativeLoggingCallback(const int logLevel, const char *const logMessage)
return;
}
/* if g_loggingCbIfaceObj has been released (part of wolfSSL_Cleanup()),
* just return and skip this log */
if (g_loggingCbIfaceObj == NULL) {
/* Store global into local ref under mutex so a concurrent cleanup() or
* setLoggingCb() can't free it while we call into Java. */
if (NativeLoggingCbLock() == 0) {
if (g_loggingCbIfaceObj != NULL) {
localCbObj = (*jenv)->NewLocalRef(jenv, g_loggingCbIfaceObj);
}
(void)NativeLoggingCbUnlock();
}
/* callback released (part of wolfSSL_Cleanup()), skip this log */
if (localCbObj == NULL) {
if (needsDetach == 1) {
(*g_vm)->DetachCurrentThread(g_vm);
}
return;
}
/* check if our stored object reference is valid */
refcheck = (*jenv)->GetObjectRefType(jenv, g_loggingCbIfaceObj);
if (refcheck == 2) {
/* Defensive check on localCbObj ref validity */
refcheck = (*jenv)->GetObjectRefType(jenv, localCbObj);
if (refcheck != JNIInvalidRefType) {
/* lookup WolfSSLLoggingCallback class from global object ref */
logClass = (*jenv)->GetObjectClass(jenv, g_loggingCbIfaceObj);
/* lookup WolfSSLLoggingCallback class from object ref */
logClass = (*jenv)->GetObjectClass(jenv, localCbObj);
if (!logClass) {
if ((*jenv)->ExceptionOccurred(jenv)) {
(*jenv)->ExceptionDescribe(jenv);
(*jenv)->ExceptionClear(jenv);
}
(*jenv)->DeleteLocalRef(jenv, localCbObj);
if (needsDetach == 1) {
(*g_vm)->DetachCurrentThread(g_vm);
}
@ -1774,11 +1896,14 @@ void NativeLoggingCallback(const int logLevel, const char *const logMessage)
logMethod = (*jenv)->GetMethodID(jenv, logClass,
"loggingCallback",
"(ILjava/lang/String;)V");
/* done with logClass, release it now */
(*jenv)->DeleteLocalRef(jenv, logClass);
if (logMethod == 0) {
if ((*jenv)->ExceptionOccurred(jenv)) {
(*jenv)->ExceptionDescribe(jenv);
(*jenv)->ExceptionClear(jenv);
}
(*jenv)->DeleteLocalRef(jenv, localCbObj);
if (needsDetach == 1) {
(*g_vm)->DetachCurrentThread(g_vm);
}
@ -1788,9 +1913,12 @@ void NativeLoggingCallback(const int logLevel, const char *const logMessage)
/* create jstring from char* */
logMsg = (*jenv)->NewStringUTF(jenv, logMessage);
(*jenv)->CallVoidMethod(jenv, g_loggingCbIfaceObj, logMethod,
(*jenv)->CallVoidMethod(jenv, localCbObj, logMethod,
logLevel, logMsg);
/* done with logMsg, release it now */
(*jenv)->DeleteLocalRef(jenv, logMsg);
if ((*jenv)->ExceptionOccurred(jenv)) {
(*jenv)->ExceptionDescribe(jenv);
(*jenv)->ExceptionClear(jenv);
@ -1799,6 +1927,7 @@ void NativeLoggingCallback(const int logLevel, const char *const logMessage)
* Otherwise, our non-important exception here could cause
* bad things to happen at the Java level - ie, causing the
* certificate verify callback to fail unnecessarily. */
(*jenv)->DeleteLocalRef(jenv, localCbObj);
if (needsDetach == 1) {
(*g_vm)->DetachCurrentThread(g_vm);
}
@ -1807,6 +1936,7 @@ void NativeLoggingCallback(const int logLevel, const char *const logMessage)
}
(*jenv)->DeleteLocalRef(jenv, localCbObj);
if (needsDetach == 1) {
(*g_vm)->DetachCurrentThread(g_vm);
}

View File

@ -1674,10 +1674,14 @@ public class WolfSSL {
/**
* Registers the callback to be used for Logging debug and trace
* messages.
* <p>
* Once a callback has been registered, passing null stops delivery to it
* and native debug output is discarded, not reverted to wolfSSL's default
* output.
*
* @param cb Callback to be used for logging debug messages
* @param cb Callback to be used for logging debug messages, or null to
* stop delivering messages to a previously registered callback
* @return <b><code>SSL_ERROR_NONE</code></b> upon success,
* <b><code>BAD_FUNC_ARG</code></b> if input is null,
* <b><code>NOT_COMPILED_IN</code></b> if wolfSSL was not
* compiled with debugging support enabled.
* @see #debuggingON()

View File

@ -31,9 +31,12 @@ import static org.junit.Assert.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import com.wolfssl.WolfSSL;
import com.wolfssl.WolfSSLException;
import com.wolfssl.WolfSSLLoggingCallback;
/* suppress SSLv3 deprecation warnings, meant for end user not tests */
@SuppressWarnings("deprecation")
@ -577,4 +580,122 @@ public class WolfSSLTest {
/* nothing */
}
}
/* Logging callback used by the setLoggingCb test. Uses a shared static
* counter so invocations across swapped instances accumulate. */
static class TestLoggingCallback implements WolfSSLLoggingCallback {
static final AtomicLong count = new AtomicLong(0);
public void loggingCallback(int logLevel, String logMessage) {
count.incrementAndGet();
}
}
@Test
public void test_WolfSSL_setLoggingCb() throws InterruptedException {
int ret;
/* Return is SSL_ERROR_NONE on a debug build, else NOT_COMPILED_IN. */
ret = WolfSSL.setLoggingCb(new TestLoggingCallback());
if (ret != WolfSSL.SSL_ERROR_NONE && ret != WolfSSL.NOT_COMPILED_IN) {
fail("WolfSSL.setLoggingCb() returned unexpected value: " + ret);
}
/* Re-register a different callback, releases the prior ref. */
ret = WolfSSL.setLoggingCb(new TestLoggingCallback());
if (ret != WolfSSL.SSL_ERROR_NONE && ret != WolfSSL.NOT_COMPILED_IN) {
fail("WolfSSL.setLoggingCb() re-register returned: " + ret);
}
ret = WolfSSL.setLoggingCb(null);
if (ret != WolfSSL.SSL_ERROR_NONE && ret != WolfSSL.NOT_COMPILED_IN) {
fail("WolfSSL.setLoggingCb(null) returned unexpected value: " +
ret);
}
WolfSSL.debuggingON();
/* Concurrency stress: writers swap and clear the callback while drivers
* make native calls that log, firing NativeLoggingCallback during the
* swap/free. Exercises both the g_loggingCbIfaceObj mutex (no jobject
* UAF) and one-time native registration (setLoggingCb(null) never nulls
* native LogFunction). Passing with no JVM abort is the assertion. */
final int numThreads = 8;
final int iterations = 500;
final AtomicReference<Throwable> firstError =
new AtomicReference<Throwable>();
Thread[] threads = new Thread[numThreads];
for (int i = 0; i < numThreads; i++) {
final int id = i;
threads[i] = new Thread(new Runnable() {
public void run() {
String role = ((id % 2) == 0) ? "writer" : "driver";
try {
for (int j = 0; j < iterations; j++) {
if ((id % 2) == 0) {
/* writer: swap in a callback, then clear it */
WolfSSL.setLoggingCb(new TestLoggingCallback());
WolfSSL.setLoggingCb(null);
}
else {
/* driver: native call that logs on debug */
long m = WolfSSL.SSLv23_ClientMethod();
if (m != 0 && m != WolfSSL.NOT_COMPILED_IN) {
WolfSSL.nativeFree(m);
}
}
}
} catch (Throwable t) {
firstError.compareAndSet(null,
new Exception("in " + role + " thread", t));
}
}
});
}
for (int i = 0; i < numThreads; i++) {
threads[i].start();
}
for (int i = 0; i < numThreads; i++) {
threads[i].join();
}
WolfSSL.debuggingOFF();
/* leave unregistered for other tests */
WolfSSL.setLoggingCb(null);
Throwable err = firstError.get();
if (err != null) {
AssertionError ae = new AssertionError(
"concurrent setLoggingCb() threw " + err.getMessage());
ae.initCause(err);
throw ae;
}
}
/* Test that NativeLoggingCallback dispatches to Java. Skips on builds
* without debug logging, or where WOLFSSL_ENTER trace messages are
* compiled out (ex: WOLFSSL_DEBUG_ERRORS_ONLY), rather than failing. */
@Test
public void test_WolfSSL_setLoggingCbDispatch() {
int ret = WolfSSL.setLoggingCb(new TestLoggingCallback());
Assume.assumeTrue("debug logging not compiled in",
ret == WolfSSL.SSL_ERROR_NONE);
WolfSSL.debuggingON();
TestLoggingCallback.count.set(0);
long m = WolfSSL.SSLv23_ClientMethod();
if (m != 0 && m != WolfSSL.NOT_COMPILED_IN) {
WolfSSL.nativeFree(m);
}
WolfSSL.debuggingOFF();
WolfSSL.setLoggingCb(null);
/* No message means this build compiled out trace logging, skip. */
Assume.assumeTrue("no trace messages emitted by this build",
TestLoggingCallback.count.get() > 0);
}
}