diff --git a/src/crypto.c b/src/crypto.c index 39f18f68..65b815a0 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -1279,7 +1279,8 @@ static CK_RV AddRSAPrivateKeyObject(WP11_Session* session, if (rv != CKR_OK) { if (*phKey != CK_INVALID_HANDLE) { /* ignore return value, logged in function */ - (void)WP11_Session_RemoveObject(session, privKeyObject); + (void)WP11_Session_RemoveObject(session, privKeyObject, + WP11_Object_OnToken(privKeyObject), 0); *phKey = CK_INVALID_HANDLE; } if (privKeyObject != NULL) { @@ -1476,7 +1477,8 @@ CK_RV C_CreateObject(CK_SESSION_HANDLE hSession, CK_ATTRIBUTE_PTR pTemplate, rv = AddObject(session, object, pTemplate, ulCount, phObject); if (rv != CKR_OK) { /* ignore return value, logged in function */ - (void)WP11_Session_RemoveObject(session, object); + (void)WP11_Session_RemoveObject(session, object, + WP11_Object_OnToken(object), 0); WP11_Object_Free(object); } @@ -1664,6 +1666,7 @@ CK_RV C_DestroyObject(CK_SESSION_HANDLE hSession, CK_OBJECT_HANDLE hObject) { int ret; + int onToken; CK_RV rv; WP11_Session* session; WP11_Object* obj = NULL; @@ -1692,29 +1695,47 @@ CK_RV C_DestroyObject(CK_SESSION_HANDLE hSession, WOLFPKCS11_LEAVE("C_DestroyObject", rv); return rv; } + /* Derive onToken from the handle, not the object: it stays valid even if a + * concurrent destroy of the same handle frees the object out from under + * us, and it lets WP11_Session_RemoveObject unlink token objects using + * pointer identity alone. */ + onToken = WP11_Object_HandleOnToken(hObject); /* Only require R/W session for token objects */ - if (!WP11_Session_IsRW(session) && WP11_Object_OnToken(obj)) { + if (!WP11_Session_IsRW(session) && onToken) { rv = CKR_SESSION_READ_ONLY; WOLFPKCS11_LEAVE("C_DestroyObject", rv); return rv; } - /* Reject destruction of objects whose CKA_DESTROYABLE has been set to - * CK_FALSE. Read the flag bit directly so the gate stays consistent - * regardless of which getter view applies. */ - if (!WP11_Object_IsDestroyable(obj)) { + /* Remove and reject-if-not-destroyable in one locked step. The + * CKA_DESTROYABLE check is performed inside WP11_Session_RemoveObject, once + * the object is confirmed still linked (and therefore alive), so it cannot + * race a concurrent free of the same handle. */ + ret = WP11_Session_RemoveObject(session, obj, onToken, + 1 /* checkDestroyable */); + if (ret == WP11_OBJECT_ALREADY_REMOVED) { + /* Another thread destroyed this object first (a concurrent + * C_DestroyObject on the same shared token-object handle). That thread + * owns the free; do not touch the object again. */ + rv = CKR_OBJECT_HANDLE_INVALID; + WOLFPKCS11_LEAVE("C_DestroyObject", rv); + return rv; + } + if (ret == WP11_OBJECT_NOT_DESTROYABLE) { + /* CKA_DESTROYABLE is CK_FALSE; object left in place. */ rv = CKR_ACTION_PROHIBITED; WOLFPKCS11_LEAVE("C_DestroyObject", rv); return rv; } - - rv = WP11_Session_RemoveObject(session, obj); /* Drop any active-operation reference to this object before freeing it so a * pending operation cannot use freed memory. */ WP11_Slot_ClearActiveObject(WP11_Session_GetSlot(session), obj); WP11_Object_Free(obj); + /* The object was unlinked; a negative status means persisting the token + * afterwards failed. Surface it rather than reporting success. */ + rv = (ret < 0) ? CKR_FUNCTION_FAILED : CKR_OK; WOLFPKCS11_LEAVE("C_DestroyObject", rv); return rv; } @@ -7915,12 +7936,14 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, if (rv != CKR_OK && pub != NULL) { if (*phPublicKey != CK_INVALID_HANDLE) - (void)WP11_Session_RemoveObject(session, pub); + (void)WP11_Session_RemoveObject(session, pub, + WP11_Object_OnToken(pub), 0); WP11_Object_Free(pub); } if (rv != CKR_OK && priv != NULL) { if (*phPrivateKey != CK_INVALID_HANDLE) - (void)WP11_Session_RemoveObject(session, priv); + (void)WP11_Session_RemoveObject(session, priv, + WP11_Object_OnToken(priv), 0); WP11_Object_Free(priv); } @@ -8418,7 +8441,8 @@ CK_RV C_UnwrapKey(CK_SESSION_HANDLE hSession, if (rv != CKR_OK) { if (*phKey != CK_INVALID_HANDLE) { /* ignore return value, logged in function */ - (void)WP11_Session_RemoveObject(session, keyObj); + (void)WP11_Session_RemoveObject(session, keyObj, + WP11_Object_OnToken(keyObj), 0); *phKey = CK_INVALID_HANDLE; } if (keyObj != NULL) { diff --git a/src/internal.c b/src/internal.c index 4077b215..1bf4072a 100644 --- a/src/internal.c +++ b/src/internal.c @@ -922,7 +922,7 @@ static void wp11_Session_Final(WP11_Session* session) /* Free objects in session. */ while ((obj = session->object) != NULL) { /* ignore return value, logged in function */ - (void)WP11_Session_RemoveObject(session, obj); + (void)WP11_Session_RemoveObject(session, obj, obj->onToken, 0); WP11_Object_Free(obj); } session->inUse = 0; @@ -8822,44 +8822,93 @@ int WP11_Session_AddObject(WP11_Session* session, int onToken, * @return -ve on failure. * 0 on success. */ -int WP11_Session_RemoveObject(WP11_Session* session, WP11_Object* object) +int WP11_Session_RemoveObject(WP11_Session* session, WP11_Object* object, + int onToken, int checkDestroyable) { int ret = 0; + int found = 0; WP11_Object** curr; - WP11_Token* token; - int id; + WP11_Token* token = &session->slot->token; + WP11_Session* owner = session; + int id = 0; + + /* The token lock guards every object list on the slot (session lists + * included). Take it before touching any list or the object. Token objects + * are shared across sessions, so two C_DestroyObject calls on the same + * handle can both pass the lock-free WP11_Object_Find and reach here with + * the same pointer; the loser must detect that the winner already unlinked + * (and is about to free) the object and bail out without a second free. + * + * Membership is established by pointer identity alone so that *object is + * never dereferenced until it is confirmed still linked (and therefore + * still alive). onToken comes from the handle, not from *object, so it is + * safe to trust even after the object has been freed. */ + WP11_Lock_LockRW(&token->lock); + if (onToken) { + /* Token objects live on the shared token list. */ + for (curr = &token->object; *curr != NULL; curr = &(*curr)->next) { + if (*curr == object) { + found = 1; + break; + } + } + } + else { + /* Session objects live on their owning session's list. Check the + * caller's own list first: it is the only possibility in the standard + * build and the common case under NSS, and needs no dereference of + * *object. */ + for (curr = &owner->object; *curr != NULL; curr = &(*curr)->next) { + if (*curr == object) { + found = 1; + break; + } + } #ifdef WOLFPKCS11_NSS - if (object->session && (session != object->session)) - session = object->session; + /* NSS makes objects visible across sessions, so the object may belong + * to a different session. Fall back to its recorded owner. This is + * reached only for a genuinely cross-session object - a freed object is + * normally caught as not-found above - and carries the same + * out-of-contract residual as a single-session concurrent misuse. */ + if (!found && object->session != NULL && object->session != session) { + owner = object->session; + for (curr = &owner->object; *curr != NULL; curr = &(*curr)->next) { + if (*curr == object) { + found = 1; + break; + } + } + } #endif + } - /* Find the object in list and relink. */ - if (object->onToken) { - WP11_Lock_LockRW(object->lock); - token = &session->slot->token; + if (!found) { + WP11_Lock_UnlockRW(&token->lock); + return WP11_OBJECT_ALREADY_REMOVED; + } + + /* The object is confirmed linked, and therefore alive, so its fields may + * now be read safely under the lock. Enforce CKA_DESTROYABLE here rather + * than in the caller so the check cannot race a concurrent free. */ + if (checkDestroyable && (object->opFlag & WP11_FLAG_NOT_DESTROYABLE)) { + WP11_Lock_UnlockRW(&token->lock); + return WP11_OBJECT_NOT_DESTROYABLE; + } + + if (onToken) { token->objCnt--; /* Id of first object on token. */ id = token->objCnt; curr = &token->object; - } - else { - session->objCnt--; - /* Id of first object in session. */ - id = session->objCnt; - curr = &session->object; - WP11_Lock_LockRW(&session->slot->token.lock); - } - - /* walk list to get id for object to remove */ - while (*curr != NULL) { - if (*curr == object) { - *curr = object->next; - break; - } + /* walk list to get id for object to remove */ + while (*curr != NULL) { + if (*curr == object) { + *curr = object->next; + break; + } - #ifndef WOLFPKCS11_NO_STORE - if (object->onToken) { + #ifndef WOLFPKCS11_NO_STORE /* remove any id's with higher value */ ret = wp11_Object_Unstore(*curr, (int)session->slotId, id); #ifdef DEBUG_WOLFPKCS11 @@ -8868,16 +8917,14 @@ int WP11_Session_RemoveObject(WP11_Session* session, WP11_Object* object) (int)session->slotId, id, ret); } #endif - } - #endif + #endif - curr = &(*curr)->next; - /* Id of next object as it isn't the one being removed. */ - id--; - } + curr = &(*curr)->next; + /* Id of next object as it isn't the one being removed. */ + id--; + } - if (object->onToken) { -#ifndef WOLFPKCS11_NO_STORE + #ifndef WOLFPKCS11_NO_STORE ret = wp11_Object_Unstore(object, (int)session->slotId, id); #ifdef DEBUG_WOLFPKCS11 if (ret != 0) { @@ -8893,12 +8940,19 @@ int WP11_Session_RemoveObject(WP11_Session* session, WP11_Object* object) (int)session->slotId, ret); } #endif -#endif - WP11_Lock_UnlockRW(object->lock); + #endif } else { - WP11_Lock_UnlockRW(&session->slot->token.lock); + owner->objCnt--; + for (curr = &owner->object; *curr != NULL; curr = &(*curr)->next) { + if (*curr == object) { + *curr = object->next; + break; + } + } } + + WP11_Lock_UnlockRW(&token->lock); (void)id; /* set but not used with WOLFPKCS11_NO_STORE */ return ret; } @@ -10197,6 +10251,22 @@ int WP11_Object_SetClass(WP11_Object* object, CK_OBJECT_CLASS objClass) return 0; } +/** + * Determine whether an object handle refers to a token object. + * + * The onToken bit is encoded in the handle value itself, so this is safe to + * call even when the WP11_Object has already been freed by another thread - + * useful when deciding which list a racing C_DestroyObject must consult. + * + * @param handle [in] Object handle. + * @return 1 when the handle refers to a token object. + * 0 when the handle refers to a session object. + */ +int WP11_Object_HandleOnToken(CK_OBJECT_HANDLE handle) +{ + return OBJ_HANDLE_ON_TOKEN(handle); +} + /** * Find an object based on the handle. * diff --git a/tests/concurrent_destroy_object_test.c b/tests/concurrent_destroy_object_test.c new file mode 100644 index 00000000..cd1b51d5 --- /dev/null +++ b/tests/concurrent_destroy_object_test.c @@ -0,0 +1,476 @@ +/* concurrent_destroy_object_test.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPKCS11. + * + * wolfPKCS11 is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfPKCS11 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + * + * Regression test for the concurrent C_DestroyObject double-free / use-after- + * free on a shared token object. + * + * A token object is visible to every session of an application, so two threads + * using two different sessions may legitimately call C_DestroyObject on the + * same object handle at the same time (a supported concurrent use under + * CKF_OS_LOCKING_OK). The buggy code resolved the handle to a raw WP11_Object* + * under a released read lock, so both callers reached WP11_Object_Free on the + * same pointer - a double free - and the loser also dereferenced the freed + * object inside WP11_Session_RemoveObject. + * + * Each round below creates one token object and has two threads (each on its + * own session) destroy it simultaneously. The fix must guarantee that exactly + * one destroy returns CKR_OK, the other returns CKR_OBJECT_HANDLE_INVALID, and + * the process does not corrupt the heap. Built with ASan this reliably aborts + * on the unfixed library. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include +#include + +#ifndef WOLFSSL_USER_SETTINGS + #include +#endif +#include +#include + +#ifndef WOLFPKCS11_USER_SETTINGS + #include +#endif +#include + +#ifndef HAVE_PKCS11_STATIC +#include +#endif + +#include "testdata.h" + +#if !defined(NO_AES) && !defined(SINGLE_THREADED) + +#include + +#define CONCURRENT_DESTROY_TEST_DIR "./store/concurrent_destroy_object_test" +#define WOLFPKCS11_TOKEN_FILENAME "wp11_token_0000000000000001" + +#define DEFAULT_ROUNDS 250 + +static int test_passed = 0; +static int test_failed = 0; + +#ifndef HAVE_PKCS11_STATIC +static void* dlib; +#endif +static CK_FUNCTION_LIST* funcList; +static CK_SLOT_ID slot = 0; +static const char* tokenName = "wolfpkcs11"; +static byte* soPin = (byte*)"password123456"; +static int soPinLen = 14; +static byte* userPin = (byte*)"wolfpkcs11-test"; +static int userPinLen = 15; + +static CK_OBJECT_CLASS secretKeyClass = CKO_SECRET_KEY; +static CK_BBOOL ckTrue = CK_TRUE; +static CK_KEY_TYPE aesKeyType = CKK_AES; + +/* Rendezvous state shared between the main thread and the two destroyers. + * A busy-wait ("spin") barrier is used deliberately: cond-var wakeups have + * enough latency that one destroyer routinely finishes before the other + * starts, which hides the race. Spinning releases both threads within a few + * cycles of each other so their C_DestroyObject calls truly overlap. */ +static volatile int g_go = 0; /* bumped by main to release a round */ +static volatile int g_done = 0; /* threads that finished current round */ +static volatile int g_stop = 0; /* set to make the threads exit */ +static CK_OBJECT_HANDLE g_target = CK_INVALID_HANDLE; +static CK_RV g_rv[2]; /* per-thread result for the round */ + +typedef struct { + int id; + CK_SESSION_HANDLE session; +} thread_ctx; + +static CK_RV pkcs11_init(void) +{ + CK_RV ret; + CK_C_INITIALIZE_ARGS args; + CK_SLOT_ID slotList[16]; + CK_ULONG slotCount = sizeof(slotList) / sizeof(slotList[0]); + +#ifndef HAVE_PKCS11_STATIC + CK_C_GetFunctionList func; + + dlib = dlopen(WOLFPKCS11_DLL_FILENAME, RTLD_NOW | RTLD_LOCAL); + if (dlib == NULL) { + fprintf(stderr, "dlopen error: %s\n", dlerror()); + return -1; + } + + func = (CK_C_GetFunctionList)dlsym(dlib, "C_GetFunctionList"); + if (func == NULL) { + fprintf(stderr, "Failed to get function list function\n"); + dlclose(dlib); + return -1; + } + + ret = func(&funcList); + if (ret != CKR_OK) { + fprintf(stderr, "Failed to get function list: 0x%lx\n", + (unsigned long)ret); + dlclose(dlib); + return ret; + } +#else + ret = C_GetFunctionList(&funcList); + if (ret != CKR_OK) { + fprintf(stderr, "Failed to get function list: 0x%lx\n", + (unsigned long)ret); + return ret; + } +#endif + + XMEMSET(&args, 0, sizeof(args)); + args.flags = CKF_OS_LOCKING_OK; + ret = funcList->C_Initialize(&args); + if (ret != CKR_OK) + return ret; + + ret = funcList->C_GetSlotList(CK_TRUE, slotList, &slotCount); + if (ret != CKR_OK) + return ret; + + if (slotCount > 0) { + slot = slotList[0]; + } else { + fprintf(stderr, "No slots available\n"); + return CKR_GENERAL_ERROR; + } + + return ret; +} + +static void pkcs11_final(void) +{ + if (funcList != NULL) { + funcList->C_Finalize(NULL); + funcList = NULL; + } +#ifndef HAVE_PKCS11_STATIC + if (dlib) { + dlclose(dlib); + dlib = NULL; + } +#endif +} + +static CK_RV pkcs11_init_token(void) +{ + unsigned char label[32]; + + XMEMSET(label, ' ', sizeof(label)); + XMEMCPY(label, tokenName, XSTRLEN(tokenName)); + + return funcList->C_InitToken(slot, soPin, soPinLen, label); +} + +static CK_RV pkcs11_set_user_pin(void) +{ + CK_SESSION_HANDLE soSession; + int sessFlags = CKF_SERIAL_SESSION | CKF_RW_SESSION; + CK_RV ret; + + ret = funcList->C_OpenSession(slot, sessFlags, NULL, NULL, &soSession); + if (ret != CKR_OK) + return ret; + + ret = funcList->C_Login(soSession, CKU_SO, soPin, soPinLen); + if (ret != CKR_OK) { + funcList->C_CloseSession(soSession); + return ret; + } + + ret = funcList->C_InitPIN(soSession, userPin, userPinLen); + funcList->C_Logout(soSession); + funcList->C_CloseSession(soSession); + return ret; +} + +static CK_RV pkcs11_open_session(CK_SESSION_HANDLE* session) +{ + CK_RV ret; + int sessFlags = CKF_SERIAL_SESSION | CKF_RW_SESSION; + + ret = funcList->C_OpenSession(slot, sessFlags, NULL, NULL, session); + if (ret != CKR_OK) + return ret; + + /* Login state is token-wide: only the first session needs to log the user + * in, later sessions inherit it and report CKR_USER_ALREADY_LOGGED_IN. */ + ret = funcList->C_Login(*session, CKU_USER, userPin, userPinLen); + if (ret != CKR_OK && ret != CKR_USER_ALREADY_LOGGED_IN) { + funcList->C_CloseSession(*session); + return ret; + } + + return CKR_OK; +} + +static void cleanup_test_files(const char* dir) +{ + char filepath[512]; + + snprintf(filepath, sizeof(filepath), "%s" PATH_SEP "%s", dir, + WOLFPKCS11_TOKEN_FILENAME); + (void)remove(filepath); +} + +/* Create an AES token secret key and return its handle. */ +static CK_RV create_token_key(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE* key) +{ + CK_ATTRIBUTE tmpl[] = { + { CKA_CLASS, &secretKeyClass, sizeof(secretKeyClass) }, + { CKA_KEY_TYPE, &aesKeyType, sizeof(aesKeyType) }, + { CKA_VALUE, aes_128_key, sizeof(aes_128_key) }, + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + }; + CK_ULONG tmplCnt = sizeof(tmpl) / sizeof(*tmpl); + + return funcList->C_CreateObject(session, tmpl, tmplCnt, key); +} + +/* Destroyer thread: each round, wait to be released, then destroy the shared + * target handle at the same moment as the peer thread. */ +static void* destroyer(void* arg) +{ + thread_ctx* ctx = (thread_ctx*)arg; + int last = 0; + + for (;;) { + CK_OBJECT_HANDLE h; + + /* Tight spin until main releases the next round (or asks us to stop). */ + while (g_go == last && !g_stop) { } + if (g_stop) + break; + last = g_go; + __sync_synchronize(); /* observe g_target published before g_go */ + h = g_target; + + g_rv[ctx->id] = funcList->C_DestroyObject(ctx->session, h); + + __sync_fetch_and_add(&g_done, 1); + } + + return NULL; +} + +static int concurrent_destroy_test(int rounds) +{ + CK_RV ret; + CK_SESSION_HANDLE creator = 0; + thread_ctx ctx[2]; + pthread_t threads[2]; + int result = 0; + int r; + int started = 0; + + printf("\n=== Testing concurrent C_DestroyObject on a shared token " + "object (%d rounds) ===\n", rounds); + + cleanup_test_files(CONCURRENT_DESTROY_TEST_DIR); + + ret = pkcs11_init(); + if (ret != CKR_OK) { + fprintf(stderr, "FAIL: pkcs11_init: 0x%lx\n", (unsigned long)ret); + test_failed++; + return -1; + } + + ret = pkcs11_init_token(); + if (ret != CKR_OK) { + fprintf(stderr, "FAIL: C_InitToken: 0x%lx\n", (unsigned long)ret); + test_failed++; + goto cleanup; + } + + ret = pkcs11_set_user_pin(); + if (ret != CKR_OK) { + fprintf(stderr, "FAIL: set user PIN: 0x%lx\n", (unsigned long)ret); + test_failed++; + goto cleanup; + } + + ret = pkcs11_open_session(&creator); + if (ret != CKR_OK) { + fprintf(stderr, "FAIL: open creator session: 0x%lx\n", + (unsigned long)ret); + test_failed++; + goto cleanup; + } + + for (r = 0; r < 2; r++) { + ctx[r].id = r; + ret = pkcs11_open_session(&ctx[r].session); + if (ret != CKR_OK) { + fprintf(stderr, "FAIL: open destroyer session %d: 0x%lx\n", r, + (unsigned long)ret); + test_failed++; + goto cleanup; + } + if (pthread_create(&threads[r], NULL, destroyer, &ctx[r]) != 0) { + fprintf(stderr, "FAIL: pthread_create %d\n", r); + test_failed++; + goto cleanup; + } + started++; + } + + for (r = 0; r < rounds; r++) { + CK_OBJECT_HANDLE h = CK_INVALID_HANDLE; + int oks; + + ret = create_token_key(creator, &h); + if (ret != CKR_OK) { + fprintf(stderr, "FAIL: create token key (round %d): 0x%lx\n", r, + (unsigned long)ret); + test_failed++; + result = -1; + break; + } + + /* Publish the handle, release both threads onto it simultaneously, + * then spin until both have returned from C_DestroyObject. */ + g_target = h; + g_rv[0] = g_rv[1] = CKR_GENERAL_ERROR; + g_done = 0; + __sync_synchronize(); + g_go = r + 1; + while (g_done < 2) { } + __sync_synchronize(); + + /* Exactly one destroy must win; the other must report an invalid + * handle. Any other combination (two successes, or a status other + * than CKR_OBJECT_HANDLE_INVALID for the loser) is a defect. */ + oks = (g_rv[0] == CKR_OK) + (g_rv[1] == CKR_OK); + if (oks != 1) { + fprintf(stderr, + "FAIL: round %d expected exactly one CKR_OK, got rv0=0x%lx " + "rv1=0x%lx\n", r, (unsigned long)g_rv[0], + (unsigned long)g_rv[1]); + test_failed++; + result = -1; + break; + } + if (g_rv[0] != CKR_OK && g_rv[0] != CKR_OBJECT_HANDLE_INVALID) { + fprintf(stderr, "FAIL: round %d loser rv0=0x%lx\n", r, + (unsigned long)g_rv[0]); + test_failed++; + result = -1; + break; + } + if (g_rv[1] != CKR_OK && g_rv[1] != CKR_OBJECT_HANDLE_INVALID) { + fprintf(stderr, "FAIL: round %d loser rv1=0x%lx\n", r, + (unsigned long)g_rv[1]); + test_failed++; + result = -1; + break; + } + + /* The object must really be gone: a follow-up destroy fails. */ + ret = funcList->C_DestroyObject(creator, h); + if (ret != CKR_OBJECT_HANDLE_INVALID) { + fprintf(stderr, + "FAIL: round %d object still present after destroy: 0x%lx\n", + r, (unsigned long)ret); + test_failed++; + result = -1; + break; + } + } + + if (result == 0) { + printf("PASS: %d concurrent-destroy rounds, one winner each, no " + "double free\n", rounds); + test_passed++; + } + +cleanup: + /* Tell the destroyer threads to exit and join them. */ + g_stop = 1; + __sync_synchronize(); + for (r = 0; r < started; r++) + pthread_join(threads[r], NULL); + for (r = 0; r < started; r++) { + funcList->C_Logout(ctx[r].session); + funcList->C_CloseSession(ctx[r].session); + } + + if (creator != 0) { + funcList->C_Logout(creator); + funcList->C_CloseSession(creator); + } + pkcs11_final(); + return result; +} + +static void print_results(void) +{ + printf("\n=== Test Results ===\n"); + printf("Tests passed: %d\n", test_passed); + printf("Tests failed: %d\n", test_failed); + + if (test_failed == 0) + printf("ALL TESTS PASSED!\n"); + else + printf("SOME TESTS FAILED!\n"); +} + +int main(int argc, char* argv[]) +{ + int rounds = DEFAULT_ROUNDS; + + if (argc > 1) { + int v = atoi(argv[1]); + if (v > 0) + rounds = v; + } + +#ifndef WOLFPKCS11_NO_ENV + XSETENV("WOLFPKCS11_TOKEN_PATH", CONCURRENT_DESTROY_TEST_DIR, 1); +#endif + + printf("=== wolfPKCS11 concurrent C_DestroyObject Test ===\n"); + + (void)concurrent_destroy_test(rounds); + + print_results(); + + return (test_failed == 0) ? 0 : 1; +} + +#else /* NO_AES || SINGLE_THREADED */ + +int main(int argc, char* argv[]) +{ + (void)argc; + (void)argv; + + printf("AES or threading not available, skipping concurrent " + "C_DestroyObject test\n"); + return 0; +} + +#endif /* !NO_AES && !SINGLE_THREADED */ diff --git a/tests/include.am b/tests/include.am index 86b3c69e..52a42e00 100644 --- a/tests/include.am +++ b/tests/include.am @@ -240,6 +240,11 @@ noinst_PROGRAMS += tests/tpm_decode_bounds_test tests_tpm_decode_bounds_test_SOURCES = tests/tpm_decode_bounds_test.c tests_tpm_decode_bounds_test_LDADD = +check_PROGRAMS += tests/concurrent_destroy_object_test +noinst_PROGRAMS += tests/concurrent_destroy_object_test +tests_concurrent_destroy_object_test_SOURCES = tests/concurrent_destroy_object_test.c +tests_concurrent_destroy_object_test_LDADD = + if BUILD_STATIC tests_pkcs11test_LDADD += src/libwolfpkcs11.la tests_pkcs11mtt_LDADD += src/libwolfpkcs11.la @@ -289,6 +294,7 @@ tests_aes_keywrap_pad_test_LDADD += src/libwolfpkcs11.la tests_mlkem_secret_scrub_test_LDADD += src/libwolfpkcs11.la tests_logout_token_key_zero_test_LDADD += src/libwolfpkcs11.la tests_tpm_decode_bounds_test_LDADD += src/libwolfpkcs11.la +tests_concurrent_destroy_object_test_LDADD += src/libwolfpkcs11.la else tests_object_id_uniqueness_test_LDADD += src/libwolfpkcs11.la tests_empty_pin_store_test_LDADD += src/libwolfpkcs11.la @@ -330,6 +336,7 @@ tests_aes_keywrap_pad_test_LDADD += src/libwolfpkcs11.la tests_mlkem_secret_scrub_test_LDADD += src/libwolfpkcs11.la tests_logout_token_key_zero_test_LDADD += src/libwolfpkcs11.la tests_tpm_decode_bounds_test_LDADD += src/libwolfpkcs11.la +tests_concurrent_destroy_object_test_LDADD += src/libwolfpkcs11.la endif EXTRA_DIST += tests/unit.h \ diff --git a/wolfpkcs11/internal.h b/wolfpkcs11/internal.h index fc9aa104..7e71d33e 100644 --- a/wolfpkcs11/internal.h +++ b/wolfpkcs11/internal.h @@ -441,7 +441,20 @@ WP11_LOCAL int WP11_Session_SetMldsaParams(WP11_Session* session, CK_VOID_PTR pa CK_ULONG paramsLen); WP11_LOCAL int WP11_Session_AddObject(WP11_Session* session, int onToken, WP11_Object* object); -WP11_LOCAL int WP11_Session_RemoveObject(WP11_Session* session, WP11_Object* object); +/* Returned by WP11_Session_RemoveObject when the object was no longer linked, + * i.e. a concurrent caller (e.g. a second C_DestroyObject on the same shared + * token-object handle) already removed it. A positive value, distinct from the + * 0 / negative store-status codes returned on a successful removal. The caller + * must not free the object a second time. */ +#define WP11_OBJECT_ALREADY_REMOVED 1 +/* Returned by WP11_Session_RemoveObject (only when checkDestroyable is set) when + * the object is still linked but has CKA_DESTROYABLE = CK_FALSE. The object is + * left in place and must not be freed. */ +#define WP11_OBJECT_NOT_DESTROYABLE 2 +WP11_LOCAL int WP11_Session_RemoveObject(WP11_Session* session, + WP11_Object* object, int onToken, + int checkDestroyable); +WP11_LOCAL int WP11_Object_HandleOnToken(CK_OBJECT_HANDLE handle); WP11_LOCAL void WP11_Slot_ClearActiveObject(WP11_Slot* slot, WP11_Object* object); WP11_LOCAL void WP11_Session_GetObject(WP11_Session* session, WP11_Object** object);