Skip to content

Fix WP11_Lock ownership and remaining TSAN races, enable TSAN in CI - #254

Merged
Frauschi merged 7 commits into
wolfSSL:masterfrom
LinuxJedi:enable-tsan-ci
Oct 19, 2026
Merged

Frauschi merged 7 commits into
wolfSSL:masterfrom
LinuxJedi:enable-tsan-ci

Conversation

@LinuxJedi

@LinuxJedi LinuxJedi commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Fixes WP11_Lock releasing 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 the WP11_Object_Find use-after-free and the WP11_Slot_Has_Empty_Pin race 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_Lock is the reader/writer lock used for the global, slot and token state. Its read path locked the write mutex 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:

  • Under Wine, pkcs11mtt hangs: with the old lock it hung until a timeout in 4 of 5 runs, with this change it passed 8 of 8.
  • ThreadSanitizer cannot track the lock, so a TSAN make check reports 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.

  • pthreads (WOLFSSL_PTHREADS): pthread_mutex_t and pthread_cond_t. The wait runs with cancellation disabled, so acquiring the lock is still not a cancellation point.
  • Windows (USE_WINDOWS_API, Vista and later): CRITICAL_SECTION and CONDITION_VARIABLE. A multi-threaded Windows build for Windows CE or with _WIN32_WINNT below 0x0600 now fails with an #error instead of falling back to the old lock.
  • Other threading APIs keep the two-mutex lock as a fallback, with a comment that it is only correct where a mutex may be released by another thread (for example a binary semaphore). WP11_NO_LOCKING builds are unchanged.

Fix remaining TSAN races and enable TSAN in sanitizer CI

  • session->curr: WP11_Slot_ClearActiveObject (from C_DestroyObject) reads and clears every session's active object under the slot lock, while each session's *Init wrote its own through WP11_Session_SetObject with no lock. WP11_Session_GetObject and WP11_Session_SetObject now take the slot lock as readers; GetObject decides whether to abort an operation whose key was destroyed under the lock and calls WP11_Session_AbortOp after releasing it. Their callers are in crypto.c and hold no locks.
  • Test harness: tests/unit.h had its own copy of the cross-thread-unlock read/write lock; it is now a start gate plus a mutex-guarded stop flag. concurrent_destroy_object_test uses __atomic acquire/release for its spin barrier instead of volatile.
  • sanitizer-tests.yml: tsan is back in the matrix; the TSAN_OPTIONS export pointing at a tsan_suppressions.txt that has never existed is removed (TSAN aborts every test at startup when its suppressions file is missing); and vm.mmap_rnd_bits is 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_test drives the cache-discard branch of WP11_Slot_Has_Empty_Pin deterministically: a DEBUG_WOLFPKCS11-only hook changes the PIN from empty to non-empty between the uncached check and caching its result, then the test checks a CKA_PRIVATE create in the public session is refused and is allowed after logging in. With the seed comparison removed it fails. It needs WP11_MIN_PIN_LEN=0, so it runs in the empty PIN store workflow and skips elsewhere.

Testing:

  • Pre-commit hook make test passes on every commit (debug and default configs).
  • After merging master: macOS TSAN make check (debug PQC) clean, 77 pass / 5 skip, including the new threaded login and logout-retire tests; ASAN make check with WP11_MIN_PIN_LEN=0, 79 pass / 3 skip.
  • Before the merge, the TSAN job was replicated in a Linux container (Debian trixie aarch64, clang 19, wolfSSL built with TSAN from the workflow's configure line, TSAN wolfTPM and ibmswtpm2 for the TPM configs): all five matrix configs clean; master in the same job failed 53 of 62 tests.
  • Windows: MinGW-w64 (ucrt64) static cross-build run under Wine: 58 pass, 4 skip, 0 fail. The #error was checked with -D_WIN32_WINNT=0x0501. MSVC was not built locally; win-test.yml covers it.
  • Interleaved A/B timing of the old and new lock (concurrent_destroy_object_test, pkcs11mtt): no measurable difference.

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.
Copilot AI balanced review requested due to automatic review settings October 15, 2026 15:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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.

Comment thread src/internal.c Outdated
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.
@LinuxJedi LinuxJedi changed the title Fix remaining TSAN races and enable TSAN in sanitizer CI Fix WP11_Lock ownership and WP11_Object_Find use-after-free, enable TSAN in CI Oct 15, 2026

@Frauschi Frauschi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/internal.c
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.
@LinuxJedi

Copy link
Copy Markdown
Member Author

@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.

  • WP11_Lock: the condition-variable lock now sits behind WP11_NO_LOCKING, for both the type and the functions; the fallback is guarded by !WP11_NO_LOCKING.
  • WP11_Object_Find: agreed. WP11_Slot_DiscardObject keeps a destroyed object allocated until the losing destroyer's object call has ended, and master already captures opFlag under the token lock, so I took master's version. b6cf59c no longer changes anything; the branch's diff against master doesn't touch WP11_Object_Find.
  • WP11_Slot_Has_Empty_Pin: master's F-8673 change does what this branch did (only cache the verdict if the PIN didn't change during the check, keyed on the seed), so I took master's version. empty_pin_cache_test and its hook stay: they drive that discard branch deterministically, and the test fails against master's code if the seed comparison is removed.
  • WP11_Session_GetObject: keeps master's abort of an operation whose key was destroyed, but reads session->curr and the abort condition under the slot lock, and calls WP11_Session_AbortOp after releasing it.

After the merge: pre-commit hook make test passes (debug 77 pass / 5 skip, default 74 / 8); macOS TSAN make check (debug PQC) is clean, 77 pass / 5 skip, including the new threaded login and logout-retire tests; ASAN make check with WP11_MIN_PIN_LEN=0 is 79 pass / 3 skip.

@LinuxJedi LinuxJedi changed the title Fix WP11_Lock ownership and WP11_Object_Find use-after-free, enable TSAN in CI Fix WP11_Lock ownership and remaining TSAN races, enable TSAN in CI Oct 16, 2026
@LinuxJedi LinuxJedi removed their assignment Oct 16, 2026
@LinuxJedi
LinuxJedi requested a review from Frauschi October 16, 2026 12:22
@Frauschi
Frauschi merged commit 9bf4f64 into wolfSSL:master Oct 19, 2026
96 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants