macOS: initialize PC/SC lazily to keep fork() children clean - #1887
Merged
Merged
Conversation
SCard::manager is a static object whose constructor called
SCardLoader::Initialize(), which establishes a PC/SC context before
main() in every VeraCrypt process. On macOS this opens an XPC
connection that starts a helper thread and marks libdispatch as
fork-unsafe.
As a result, CoreService::Start() forked a multithreaded process, and
the FUSE service (which libfuse runs after fork() without exec())
inherited armed Objective-C fork-safety checks and poisoned dispatch
queues. With macFUSE >= 5.3.3 this causes:
- a SIGABRT when mounting ("+[NSNumber initialize] may have been in
progress in another thread when fork() was called"), and
- a SIGSEGV in MFChannelClose/dispatch_channel_cancel at unmount.
Load the PC/SC library on first use instead: GetReaders() now calls
loader->Initialize() itself (GetReader() already did, and Initialize()
is idempotent). PC/SC is then only touched when EMV keyfiles are used,
and never in the core service or FUSE service processes.
Tested on macOS 27.0 (arm64) with macFUSE 5.4.0: 30/30 mount/write/
remount/verify/dismount cycles with no crash reports. Previously every
mount failed.
Refs veracrypt#1884, veracrypt#1863, macfuse/macfuse#1193
Assisted-by: Claude Opus 5.5
idrassi
force-pushed
the
macos-lazy-pcsc-init
branch
from
September 25, 2026 07:11
f210530 to
a62862d
Compare
Member
|
Thank you for the fix and the detailed investigation and testing. I’m merging this PR. I amended the commit message to replace the AI |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I can confirm @bfleischer's PCSC analysis, and making the PC/SC initialization lazy fixes both the mount-time SIGABRT (veracrypt/VeraCrypt#1884) and the unmount-time SIGSEGV (#1863) in my testing, with no environment variable workaround.
Environment
master@b48e31f(1.26.29), Xcode 27.0 / SDK 27.0,build_veracrypt_macosx.sh -lRoot cause
Common/SCard.cppdefines a staticSCardManager SCard::manager;. Its constructor callsSCardLoader::Initialize(), which callsSCardEstablishContext(), so this runs during static initialization, beforemain(), in every VeraCrypt process, even when no smart card is ever used. On macOS this opens an XPC connection, which:fork()inCoreService::Start()happens in a multithreaded process. That arms the Objective-C+initializefork-safety check in the core service and every process forked from it, including the FUSE service. When libfuse creates its DiskArbitration session after daemonizing, the child aborts with+[NSNumber initialize] may have been in progress in another thread when fork() was called.0x100). That explains the_dispatch_root_queue_pushfault at0x110inMFChannelCloseat unmount (SIGSEGV (use-after-free) in FUSE session teardown: MFMount Channel.close / dispatch_channel_cancel #1863).I measured this with temporary instrumentation (
task_threads()right before eachfork()) during one CLI mount:CoreService::StartProcess::Execute(FUSE service)Fix
SCardManager's constructor no longer initializes the loader.SCardManager::GetReaders()now callsloader->Initialize()itself (GetReader()already did, andInitialize()is idempotent).EMVToken.cppcallsGetReaders()directly, which is why it needs the call.Finalize()in the destructor is already safe when nothing was initialized. So PC/SC is only loaded when EMV keyfiles are actually used, and then only in the UI process. The core service and FUSE service processes never touch it.SCardManager::SCardManager() { -#ifndef TC_OPENBSD - loader->Initialize(); -#endif + // The PC/SC library is loaded lazily on first use (see GetReaders/GetReader). + // ... (explanatory comment) } @@ SCardManager::GetReaders() LONG lRet = SCARD_S_SUCCESS; + loader->Initialize(); + hScardContext = loader->GetSCardContext();(The OpenBSD guard only existed in the constructor.
GetReader()already calledInitialize()unconditionally on all platforms, so behaviour there is unchanged.)Test results
CLI, 20 MB test volume, each cycle: mount → write 1 MiB random data → dismount → mount → verify SHA-1 → dismount, no
OBJC_DISABLE_INITIALIZE_FORK_SAFETY:OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YESThe GUI starts normally and produces no crash reports.
--list-emvtoken-keyfilesbehaves the same as before, but I have no smart card reader, so I could not test the EMV path with a real card. That would be worth checking before merging.Not tested: mounting through the elevated (admin) core service path. The fix does not change it, and
main()goes intoProcessElevatedRequests()without any PC/SC usage either way.Remaining note
The FUSE service still runs after
fork()withoutexec(), which Apple documents as unsupported. With this fix the forks happen in single-threaded processes with untouched framework state, which is the precondition macFUSE ≥ 5.3.3 expects. Longer term, starting the FUSE service viaexec/posix_spawnwould remove the dependency on this entirely.Patch attached, and I'm happy to open a PR.
SCard::manager is a static object whose constructor called SCardLoader::Initialize(), which establishes a PC/SC context before main() in every VeraCrypt process. On macOS this opens an XPC connection that starts a helper thread and marks libdispatch as fork-unsafe.
As a result, CoreService::Start() forked a multithreaded process, and the FUSE service (which libfuse runs after fork() without exec()) inherited armed Objective-C fork-safety checks and poisoned dispatch queues. With macFUSE >= 5.3.3 this causes:
Load the PC/SC library on first use instead: GetReaders() now calls loader->Initialize() itself (GetReader() already did, and Initialize() is idempotent). PC/SC is then only touched when EMV keyfiles are used, and never in the core service or FUSE service processes.
Tested on macOS 27.0 (arm64) with macFUSE 5.4.0: 30/30 mount/write/ remount/verify/dismount cycles with no crash reports. Previously every mount failed.
Refs #1884, #1863, macfuse/macfuse#1193