Skip to content

macOS: initialize PC/SC lazily to keep fork() children clean - #1887

Merged
idrassi merged 1 commit into
veracrypt:masterfrom
heinz-goetz:macos-lazy-pcsc-init
Sep 25, 2026
Merged

idrassi merged 1 commit into
veracrypt:masterfrom
heinz-goetz:macos-lazy-pcsc-init

Conversation

@heinz-goetz

@heinz-goetz heinz-goetz commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

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

  • macOS 27.0 (26A428), MacBookPro18,1 (Apple silicon, arm64)
  • macFUSE 5.4.0
  • VeraCrypt built from master @ b48e31f (1.26.29), Xcode 27.0 / SDK 27.0, build_veracrypt_macosx.sh -l

Root cause

Common/SCard.cpp defines a static SCardManager SCard::manager;. Its constructor calls SCardLoader::Initialize(), which calls SCardEstablishContext(), so this runs during static initialization, before main(), in every VeraCrypt process, even when no smart card is ever used. On macOS this opens an XPC connection, which:

  1. starts an extra thread, so the first fork() in CoreService::Start() happens in a multithreaded process. That arms the Objective-C +initialize fork-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.
  2. marks libdispatch as fork-unsafe, so the forked children get poisoned dispatch queues (tail pointer 0x100). That explains the _dispatch_root_queue_push fault at 0x110 in MFChannelClose at 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 each fork()) during one CLI mount:

Fork site Unmodified With fix
CoreService::Start 2 threads 1 thread
Process::Execute (FUSE service) 1 1
FuseService signal-handler fork 1 1
Mount result fails (SIGABRT) succeeds

Fix

SCardManager's constructor no longer initializes the loader. SCardManager::GetReaders() now calls loader->Initialize() itself (GetReader() already did, and Initialize() is idempotent). EMVToken.cpp calls GetReaders() 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 called Initialize() 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:

Build Result Crash reports
unmodified mount fails 1/1 (SIGABRT) 1
unmodified + OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES 5/5 mount OK 1 × SIGSEGV at unmount (#1863)
with fix 30/30 OK (10 + 20 cycles), data verified 0

The GUI starts normally and produces no crash reports. --list-emvtoken-keyfiles behaves 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 into ProcessElevatedRequests() without any PC/SC usage either way.

Remaining note

The FUSE service still runs after fork() without exec(), 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 via exec/posix_spawn would 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:

  • 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 #1884, #1863, macfuse/macfuse#1193

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
idrassi force-pushed the macos-lazy-pcsc-init branch from f210530 to a62862d Compare September 25, 2026 07:11
@idrassi

idrassi commented Sep 25, 2026

Copy link
Copy Markdown
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 Co-Authored-By: trailer with Assisted-by:. VeraCrypt reserves authorship and co-authorship for human contributors. AI tools can be acknowledged for their assistance. Your authorship is preserved, and this edit only changes how the AI assistance is credited.

@idrassi
idrassi merged commit 55920de into veracrypt:master Sep 25, 2026
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.

2 participants