Skip to content

feat: add ZTS support and persistent HSM session pooling#81

Open
mroest wants to merge 1 commit into
gamringer:masterfrom
mroest:feature/php-zts
Open

feat: add ZTS support and persistent HSM session pooling#81
mroest wants to merge 1 commit into
gamringer:masterfrom
mroest:feature/php-zts

Conversation

@mroest
Copy link
Copy Markdown

@mroest mroest commented Apr 18, 2026

Closes #80

Implement thread-safe PKCS#11 bindings and a per-thread session pool to reduce the cost of repeated C_OpenSession calls against hardware security modules (HSMs). Motivated by FrankenPHP worker-mode, where PHP threads are long-lived and session setup latency dominates.

Process-wide library registry (pkcs11module.c)

  • pkcs11_lib_record holds dlhandle, functionList and a finalized flag
  • HashTable pkcs11_libs keyed by realpath, protected by pthread_mutex
  • Double-checked locking in Module::__construct prevents duplicate C_Initialize calls across threads
  • Library lifetime is module lifetime; C_Finalize/dlclose in MSHUTDOWN

Per-thread session pool (php_pkcs11.h, pkcs11session.c)

  • ZEND_MODULE_GLOBALS adds a session_pool HashTable per thread
  • Pool key: "realpath:slotID:flags"; one entry per key (no linked list)
  • Pool holds unauthenticated sessions only: C_Logout is called before returning a handle to the pool, so every reuse requires fresh login
  • Tainted sessions (active C_*Init in flight) are closed on GC, never pooled, preventing state leakage across requests

Stale-session resilience (pkcs11int.h)

  • PKCS11_SESSION_CALL: wraps C_*Init and single-shot ops; on stale errors (CKR_SESSION_HANDLE_INVALID etc.) evicts the pool entry, opens a fresh session, and retries the expression once
  • PKCS11_SESSION_EVICT: wraps C_*Update/C_*Final; evicts only because a new session cannot replay prior Init/Update state

Module lifecycle (pkcs11.c)

  • PHP_MINIT initialises library registry and module globals
  • PHP_MSHUTDOWN sets finalized=true on each lib record before C_Finalize so that globals_dtor can skip C_CloseSession safely regardless of shutdown order
  • Non-ZTS build: globals_dtor called explicitly in MSHUTDOWN because ZEND_INIT_MODULE_GLOBALS ignores the destructor argument there
  • ZTS C_Initialize passes CKF_OS_LOCKING_OK; non-ZTS passes NULL

All call sites wrapped (pkcs11key.c, pkcs11session.c, context files, pkcs11object.c, pkcs11digestcontext.c)

Tests added (tests/0500-0511.phpt)

  • 0500: module object reuse across requests
  • 0501: failed constructor does not under-count
  • 0502: session pool reuse across requests
  • 0503: wrong credentials rejected on fresh and pooled sessions
  • 0504: GC cleanup does not crash; re-creation works
  • 0505: stale pool entry skipped via dead flag
  • 0506: separate library paths use independent pool entries
  • 0507: tainted session is closed, not pooled
  • 0508: ephemeral session issued when pool entry is in use
  • 0509: single-shot sign clears taint; pool reuse succeeds
  • 0510: wrong PIN on fresh session throws; correct PIN works
  • 0511: login succeeds after pool reuse, proving C_Logout ran

Implement thread-safe PKCS#11 bindings and a per-thread session pool
to reduce the cost of repeated C_OpenSession calls against hardware
security modules (HSMs).  Motivated by FrankenPHP worker-mode, where
PHP threads are long-lived and session setup latency dominates.

Process-wide library registry (pkcs11module.c)
- pkcs11_lib_record holds dlhandle, functionList and a finalized flag
- HashTable pkcs11_libs keyed by realpath, protected by pthread_mutex
- Double-checked locking in Module::__construct prevents duplicate
  C_Initialize calls across threads
- Library lifetime is module lifetime; C_Finalize/dlclose in MSHUTDOWN

Per-thread session pool (php_pkcs11.h, pkcs11session.c)
- ZEND_MODULE_GLOBALS adds a session_pool HashTable per thread
- Pool key: "realpath:slotID:flags"; one entry per key (no linked list)
- Pool holds unauthenticated sessions only: C_Logout is called before
  returning a handle to the pool, so every reuse requires fresh login
- Tainted sessions (active C_*Init in flight) are closed on GC, never
  pooled, preventing state leakage across requests

Stale-session resilience (pkcs11int.h)
- PKCS11_SESSION_CALL: wraps C_*Init and single-shot ops; on stale
  errors (CKR_SESSION_HANDLE_INVALID etc.) evicts the pool entry,
  opens a fresh session, and retries the expression once
- PKCS11_SESSION_EVICT: wraps C_*Update/C_*Final; evicts only because
  a new session cannot replay prior Init/Update state

Module lifecycle (pkcs11.c)
- PHP_MINIT initialises library registry and module globals
- PHP_MSHUTDOWN sets finalized=true on each lib record before
  C_Finalize so that globals_dtor can skip C_CloseSession safely
  regardless of shutdown order
- Non-ZTS build: globals_dtor called explicitly in MSHUTDOWN because
  ZEND_INIT_MODULE_GLOBALS ignores the destructor argument there
- ZTS C_Initialize passes CKF_OS_LOCKING_OK; non-ZTS passes NULL

All call sites wrapped (pkcs11key.c, pkcs11session.c, context files,
pkcs11object.c, pkcs11digestcontext.c)

Tests added (tests/0500-0511.phpt)
- 0500: module object reuse across requests
- 0501: failed constructor does not under-count
- 0502: session pool reuse across requests
- 0503: wrong credentials rejected on fresh and pooled sessions
- 0504: GC cleanup does not crash; re-creation works
- 0505: stale pool entry skipped via dead flag
- 0506: separate library paths use independent pool entries
- 0507: tainted session is closed, not pooled
- 0508: ephemeral session issued when pool entry is in use
- 0509: single-shot sign clears taint; pool reuse succeeds
- 0510: wrong PIN on fresh session throws; correct PIN works
- 0511: login succeeds after pool reuse, proving C_Logout ran
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.

ZTS (thread-safe) support + persistent session pool

1 participant