Never destroy module-wide singletons via static teardown - #897
Never destroy module-wide singletons via static teardown#897rikard-soderstrom wants to merge 1 commit into
Conversation
SoftHSM::instance, MutexFactory::instance, SecureMemoryRegistry::instance, and OSSLCryptoFactory::instance/BotanCryptoFactory::instance were each a class-level static std::unique_ptr<T> (or std::auto_ptr<T> pre-C++11). Their destructors ran as part of the C++ runtime's ordinary static-object teardown at process exit - an indeterminate time relative to unrelated code's own cleanup. Concretely: a PKCS#11 caller (e.g. an OpenSSL provider) that still holds open sessions at process exit calls C_CloseSession() on each before calling C_Finalize(), per the PKCS#11 spec. But SoftHSM::instance's own destructor could fire first, via ordinary static teardown, independently of and unordered with respect to that caller's explicit cleanup sequence. Whichever teardown path lost the race operated on an already-destroyed (or transparently reconstructed-blank, since SoftHSM::i() rebuilds a fresh instance on next access) singleton, and its stale session-close calls then dereferenced a NULL member on it - a real, reproducible SIGSEGV when a pkcs11-provider-based caller runs openssl req against a PKCS#11 key and exits normally. This is the same category of bug already fixed once for OSSLCryptoFactory's own OpenSSL ENGINE cleanup (issue softhsm#548, commit c2cc065), just never applied to these singletons' own top-level lifecycle - and the gap resurfaces one level down too: OSSLCryptoFactory can be lazily constructed for the first time from deep inside SoftHSM::C_Finalize()'s own teardown chain, touching ENGINE_by_id() after OpenSSL's own OPENSSL_cleanup() has already run. Each singleton's std::unique_ptr<T>/std::auto_ptr<T> is now heap-allocated once and deliberately never freed, so nothing destroys the T it owns except that class's own explicit reset()/C_Finalize() path - never ordinary static teardown. The smart pointer itself is retained as the owning type (matching the "Static and Global Variables" guidance in the Google C++ Style Guide, and the same "leaky singleton" idiom used by e.g. Drake's never_destroyed<T>), so reset()'s exception-safety and ownership semantics for the owned object are unchanged.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (17)
Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughSingleton storage changed from direct smart-pointer objects to pointers to dynamically allocated smart-pointer wrappers. Production accessors, reset paths, utility code, and tests support both C++11 and legacy builds. ChangesSingleton holder migration
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to The change moves singleton destruction to explicit lifecycle paths, preventing unsafe process-exit teardown ordering without changing public behavior. No actionable merge-blocking risk remains after normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
SoftHSM::instance, MutexFactory::instance, SecureMemoryRegistry::instance, and OSSLCryptoFactory::instance/BotanCryptoFactory::instance were each a class-level static std::unique_ptr (or std::auto_ptr pre-C++11). Their destructors ran as part of the C++ runtime's ordinary static-object teardown at process exit - an indeterminate time relative to unrelated code's own cleanup.
Concretely: a PKCS#11 caller (e.g. an OpenSSL provider) that still holds open sessions at process exit calls C_CloseSession() on each before calling C_Finalize(), per the PKCS#11 spec. But SoftHSM::instance's own destructor could fire first, via ordinary static teardown, independently of and unordered with respect to that caller's explicit cleanup sequence. Whichever teardown path lost the race operated on an already-destroyed (or transparently reconstructed-blank, since SoftHSM::i() rebuilds a fresh instance on next access) singleton, and its stale session-close calls then dereferenced a NULL member on it - a real, reproducible SIGSEGV when a pkcs11-provider-based caller runs openssl req against a PKCS#11 key and exits normally.
This is the same category of bug already fixed once for OSSLCryptoFactory's own OpenSSL ENGINE cleanup (issue #548, commit c2cc065), just never applied to these singletons' own top-level lifecycle - and the gap resurfaces one level down too: OSSLCryptoFactory can be lazily constructed for the first time from deep inside SoftHSM::C_Finalize()'s own teardown chain, touching ENGINE_by_id() after OpenSSL's own OPENSSL_cleanup() has already run.
Each singleton's std::unique_ptr/std::auto_ptr is now heap-allocated once and deliberately never freed, so nothing destroys the T it owns except that class's own explicit reset()/C_Finalize() path - never ordinary static teardown. The smart pointer itself is retained as the owning type (matching the "Static and Global Variables" guidance in the Google C++ Style Guide, and the same "leaky singleton" idiom used by e.g. Drake's never_destroyed), so reset()'s exception-safety and ownership semantics for the owned object are unchanged.
Summary by CodeRabbit
Bug Fixes
Tests