Repository navigation
Fix WP11_Lock ownership and remaining TSAN races, enable TSAN in CI - #254
Conversation
WP11_Lock_LockRO locked the write mutex for the first reader and WP11_Lock_UnlockRO unlocked it for the last reader, which is often a different thread. Unlocking a pthread mutex, or leaving a critical section, from a thread that does not own it is undefined behaviour. Under Wine pkcs11mtt hangs because of it, and ThreadSanitizer cannot track the lock at all. Rebuild the lock from one mutex and a condition variable, with a reader count and a writer flag. The mutex only guards the counters and is not held while the lock is held. The semantics are unchanged: a waiting writer does not block new readers, so a thread can take the read lock again while holding it. pthreads use pthread_mutex_t and pthread_cond_t; Windows (Vista and later) uses CRITICAL_SECTION and CONDITION_VARIABLE. The two-mutex lock remains as a fallback for other threading APIs.
WP11_Object_Find walked the object list under the token lock, released the lock, and then read obj->handle and obj->opFlag. A concurrent C_DestroyObject of the same token object from another session could unlink and free the object in that window, which ASAN caught in concurrent_destroy_object_test. Read everything needed from the object, including the CKA_PRIVATE flag, while the token lock is held, and do not dereference it after the unlock. The slot lock used for the login state check is still only taken after the token lock is released, so the slot-then-token lock order is unchanged.
WP11_Slot_ClearActiveObject reads and clears every session's active object under the slot lock, but each session wrote its own with no lock. Access session->curr under the slot lock in WP11_Session_GetObject and WP11_Session_SetObject. WP11_Slot_Has_Empty_Pin cached userPinEmpty without a lock. Read the PIN state under the slot lock and store the cached result under it, and only if the user PIN did not change while the expensive check ran with the lock released. tests/unit.h had its own copy of the cross-thread-unlock read/write lock; replace it with a start gate and a mutex-guarded stop flag. concurrent_destroy_object_test now uses atomic loads and stores for its spin barrier. Add tsan back to the sanitizer matrix. Its make check step pointed TSAN_OPTIONS at a suppressions file that does not exist, which makes every test abort at startup, so drop it. Also lower vm.mmap_rnd_bits for TSAN, whose fixed shadow mappings can collide with the higher ASLR entropy of current runner kernels.
There was a problem hiding this comment.
🔵 Needs a closer look
Cross-platform locking and authorization-sensitive PIN caching need human concurrency review, including the missing cache-invalidation regression test.
1 open finding
What changed in this PR
Addresses synchronization races in wolfPKCS11 and its test harness, and restores ThreadSanitizer (TSAN) CI coverage. Includes the prerequisite lock and object-lookup fixes from #253 and #251.
Changes:
- Updates library locking and protects active-object and PIN-cache access.
- Synchronizes test startup, stopping, and concurrent-destroy barriers.
- Enables TSAN with runner memory-layout adjustments and removes the missing suppression-file reference.
| File | Description |
|---|---|
| tests/unit.h | Replaces harness locking with a start gate and synchronized stop flag. |
| tests/concurrent_destroy_object_test.c | Uses atomic operations for thread coordination. |
| src/internal.c | Fixes lock ownership, object lookup, and shared-state synchronization. |
| .github/workflows/sanitizer-tests.yml | Restores TSAN and adjusts its runner configuration. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
pthread_cond_wait() is a cancellation point, but acquiring a WP11_Lock was not one when it was built from mutexes alone. A thread cancelled while waiting for the lock would exit holding the lock's mutex and hang every later user of the lock, including the current holder's unlock. Disable cancellation around the wait, restoring the previous behaviour. The guard on PTHREAD_CANCEL_DISABLE keeps this building on pthreads implementations without cancellation support.
WP11_Slot_Has_Empty_Pin only caches its result if the user PIN did not change while the expensive check ran with the slot lock released. Add a test for that branch: a DEBUG_WOLFPKCS11-only hook, WP11_Test_SetEmptyPinCheckHook, runs between the check and the caching, and empty_pin_cache_test uses it to change the PIN from empty to non-empty at exactly that point. A later CKA_PRIVATE object creation in the public session must then be refused, and allowed after logging in with the new PIN. Without the PIN comparison the stale "empty" result is cached and the creation succeeds. The test needs an empty user PIN (WP11_MIN_PIN_LEN=0, the default for NSS builds) and reports itself skipped otherwise, so run it in the empty PIN store workflow, which builds that way.
Frauschi
left a comment
There was a problem hiding this comment.
This needs a rebase: since it branched, master picked up #227/#243/#252, and there are conflicts in src/internal.c, wolfpkcs11/internal.h and empty-pin-store-test.yml. The lock rewrite now has to fit around WP11_NO_LOCKING (aebbd51).
Also worth checking during the rebase: master now defers freeing destroyed objects until no object call can still be using them (WP11_Slot_DiscardObject, from ba08cd4). That looks like it already covers the WP11_Object_Find use-after-free, so b6cf59c may no longer be needed, or could be reduced to the CKA_PRIVATE capture.
Conflict resolutions: - WP11_Lock: the mutex and condition variable lock now sits behind master's WP11_NO_LOCKING instead of SINGLE_THREADED, for both the type and the functions. - WP11_Object_Find: take master's version. Master defers freeing a destroyed object until no object call can still be using it (WP11_Slot_DiscardObject) and already captures opFlag under the token lock, which covers what this branch's lookup fix did. - WP11_Slot_Has_Empty_Pin: take master's version (it only caches the verdict if the PIN seed is unchanged) and keep the debug-only hook that empty_pin_cache_test uses, between the check and the caching. - WP11_Session_GetObject: read session->curr and decide whether to abort the operation under the slot lock, then call master's WP11_Session_AbortOp after releasing it. - internal.h and empty-pin-store-test.yml: keep both sides.
A multi-threaded Windows build that targets Windows CE or sets _WIN32_WINNT below 0x0600 silently fell back to the two-mutex lock, which leaves a critical section from a thread that did not enter it. Fail the build with an explanation instead. Single-threaded builds are unaffected.
|
@Frauschi thanks. I merged master into the branch (56be05a) rather than rebasing, to avoid force-pushing an open PR; happy to rebase instead if you'd prefer a linear history.
After the merge: pre-commit hook |

Fixes
WP11_Lockreleasing a lock from a thread that did not take it, fixes the remaining races ThreadSanitizer then reports, and turns TSAN back on in CI. This supersedes #251 and #253; #252 has been merged separately.The branch has master merged in (56be05a). Since it was opened, master gained a deferred free for destroyed objects (
WP11_Slot_DiscardObject) and its own fix for the empty-PIN cache (F-8673), which cover theWP11_Object_Finduse-after-free and theWP11_Slot_Has_Empty_Pinrace this PR originally fixed. The merge takes master's versions of both, so the diff against master no longer touches them.Stop releasing WP11_Lock from a thread that did not take it
WP11_Lockis the reader/writer lock used for the global, slot and token state. Its read path locked thewritemutex for the first reader and unlocked it for the last reader, which is often a different thread. Releasing a pthread mutex, or leaving a critical section, from a thread that does not own it is undefined behaviour:pkcs11mtthangs: with the old lock it hung until a timeout in 4 of 5 runs, with this change it passed 8 of 8.make checkreports hundreds of false races and lock-order inversions, even in single-threaded tests.The lock is now one mutex plus a condition variable, with a reader count and a writer flag. The mutex only guards the counters and is never held while the lock is held. The semantics are unchanged: a waiting writer does not stop new readers, so a thread can take the read lock again while holding it.
WOLFSSL_PTHREADS):pthread_mutex_tandpthread_cond_t. The wait runs with cancellation disabled, so acquiring the lock is still not a cancellation point.USE_WINDOWS_API, Vista and later):CRITICAL_SECTIONandCONDITION_VARIABLE. A multi-threaded Windows build for Windows CE or with_WIN32_WINNTbelow 0x0600 now fails with an#errorinstead of falling back to the old lock.WP11_NO_LOCKINGbuilds are unchanged.Fix remaining TSAN races and enable TSAN in sanitizer CI
session->curr:WP11_Slot_ClearActiveObject(fromC_DestroyObject) reads and clears every session's active object under the slot lock, while each session's*Initwrote its own throughWP11_Session_SetObjectwith no lock.WP11_Session_GetObjectandWP11_Session_SetObjectnow take the slot lock as readers;GetObjectdecides whether to abort an operation whose key was destroyed under the lock and callsWP11_Session_AbortOpafter releasing it. Their callers are incrypto.cand hold no locks.tests/unit.hhad its own copy of the cross-thread-unlock read/write lock; it is now a start gate plus a mutex-guardedstopflag.concurrent_destroy_object_testuses__atomicacquire/release for its spin barrier instead ofvolatile.sanitizer-tests.yml:tsanis back in the matrix; theTSAN_OPTIONSexport pointing at atsan_suppressions.txtthat has never existed is removed (TSAN aborts every test at startup when its suppressions file is missing); andvm.mmap_rnd_bitsis lowered for TSAN only, since its fixed shadow mappings can collide with the higher ASLR entropy of current runner kernels.Empty-PIN cache test
tests/empty_pin_cache_testdrives the cache-discard branch ofWP11_Slot_Has_Empty_Pindeterministically: aDEBUG_WOLFPKCS11-only hook changes the PIN from empty to non-empty between the uncached check and caching its result, then the test checks aCKA_PRIVATEcreate in the public session is refused and is allowed after logging in. With the seed comparison removed it fails. It needsWP11_MIN_PIN_LEN=0, so it runs in the empty PIN store workflow and skips elsewhere.Testing:
make testpasses on every commit (debug and default configs).make check(debug PQC) clean, 77 pass / 5 skip, including the new threaded login and logout-retire tests; ASANmake checkwithWP11_MIN_PIN_LEN=0, 79 pass / 3 skip.#errorwas checked with-D_WIN32_WINNT=0x0501. MSVC was not built locally;win-test.ymlcovers it.concurrent_destroy_object_test,pkcs11mtt): no measurable difference.