From af355ccf9c8c88444ea245786e475280b017533d Mon Sep 17 00:00:00 2001 From: Damian Rickard Date: Tue, 7 Jul 2026 21:04:51 -0400 Subject: [PATCH] Auto-dismount volumes on system sleep and screen lock on macOS On macOS, mounted volumes stayed mounted (and their keys resident in memory) when the system went to sleep or the screen was locked. The existing auto-dismount triggers do not cover these on macOS: - Power-suspend dismount is connected only under wxHAS_POWER_EVENTS, which wxWidgets does not define on macOS, so OnPowerSuspending never fires. - Screen-saver dismount in MainFrame::OnTimer is inside #ifdef TC_WINDOWS (SPI_GETSCREENSAVERRUNNING) and has no macOS path. Add a small Objective-C++ helper (MacOSXSleepLock) that observes NSWorkspaceWillSleepNotification and the com.apple.screenIsLocked distributed notification and calls back into GraphicUserInterface, gated by the existing DismountOnPowerSaving / DismountOnScreenSaver / BackgroundTaskEnabled preferences. The observer is installed in OnInit and removed in the destructor. Details: - The notification callbacks are exception boundaries: no C++ exception can unwind into AppKit notification dispatch (GetMountedVolumes and the dismount path can throw). The dismount is best-effort; UI cannot be shown reliably while the session is locking or the system is going to sleep. - com.apple.screenIsLocked is a de facto but undocumented distributed notification that any local process can post. It is treated only as a hint: the handler verifies via CGSessionCopyCurrentDictionary that the session is actually locked before dismounting, re-checking once after a short delay to tolerate notification/state ordering. - The dismount honors the ForceAutoDismount preference (alwaysForce = false), matching the Windows auto-dismount events, which all pass bForceAutoDismount. Volumes with open files are only detached if the user enabled forced auto-dismount. - The two preference checkboxes, previously hidden on macOS, are now shown. Because the checkbox drives screen-lock dismount on macOS, it is relabeled with the existing IDC_PREF_UNMOUNT_SESSION_LOCKED string (translated in all languages). The power-saving checkbox referenced the nonexistent key LINUX_ENTERING_POVERSAWING; point it at the correctly spelled LINUX_ENTERING_POWERSAVING, which exists in Language.xml and all translations (this also fixes the label on Linux builds that show this checkbox). All new code is macOS-only (guarded by TC_MACOSX and built solely via the MacOSX block of Main.make); the only cross-platform change is the Forms label key correction. No new framework link flags are needed: ApplicationServices/CoreGraphics symbols already resolve through the wxWidgets link line. Note: sleep dismount is best-effort within the short window NSWorkspace grants before sleep; delaying sleep via an IOKit power assertion to guarantee completion could be a follow-up. --- src/Main/Forms/Forms.cpp | 2 +- src/Main/Forms/PreferencesDialog.cpp | 8 +- src/Main/Forms/TrueCrypt.fbp | 2 +- src/Main/GraphicUserInterface.cpp | 39 ++++++++ src/Main/MacOSXSleepLock.h | 28 ++++++ src/Main/MacOSXSleepLock.mm | 133 +++++++++++++++++++++++++++ src/Main/Main.make | 1 + 7 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 src/Main/MacOSXSleepLock.h create mode 100644 src/Main/MacOSXSleepLock.mm diff --git a/src/Main/Forms/Forms.cpp b/src/Main/Forms/Forms.cpp index c64dbbfbc4..4f2fb5923a 100644 --- a/src/Main/Forms/Forms.cpp +++ b/src/Main/Forms/Forms.cpp @@ -1832,7 +1832,7 @@ PreferencesDialogBase::PreferencesDialogBase( wxWindow* parent, wxWindowID id, c DismountOnScreenSaverCheckBox = new wxCheckBox( sbSizer13->GetStaticBox(), wxID_ANY, _("IDC_PREF_UNMOUNT_SCREENSAVER"), wxDefaultPosition, wxDefaultSize, 0 ); sbSizer13->Add( DismountOnScreenSaverCheckBox, 0, wxALL, 5 ); - DismountOnPowerSavingCheckBox = new wxCheckBox( sbSizer13->GetStaticBox(), wxID_ANY, _("LINUX_ENTERING_POVERSAWING"), wxDefaultPosition, wxDefaultSize, 0 ); + DismountOnPowerSavingCheckBox = new wxCheckBox( sbSizer13->GetStaticBox(), wxID_ANY, _("LINUX_ENTERING_POWERSAVING"), wxDefaultPosition, wxDefaultSize, 0 ); sbSizer13->Add( DismountOnPowerSavingCheckBox, 0, wxALL, 5 ); diff --git a/src/Main/Forms/PreferencesDialog.cpp b/src/Main/Forms/PreferencesDialog.cpp index 879394cac7..3d2a8d733e 100644 --- a/src/Main/Forms/PreferencesDialog.cpp +++ b/src/Main/Forms/PreferencesDialog.cpp @@ -295,12 +295,16 @@ namespace VeraCrypt CloseExplorerWindowsOnDismountCheckBox->Show (false); #endif -#ifndef wxHAS_POWER_EVENTS + // On macOS, wxHAS_POWER_EVENTS is undefined but sleep is handled by a + // native observer (see MacOSXSleepLock), so keep the checkbox visible. +#if !defined(wxHAS_POWER_EVENTS) && !defined(TC_MACOSX) DismountOnPowerSavingCheckBox->Show (false); #endif #ifdef TC_MACOSX - DismountOnScreenSaverCheckBox->Show (false); + // On macOS this checkbox drives dismount on screen lock (see + // MacOSXSleepLock), so relabel it accordingly. + DismountOnScreenSaverCheckBox->SetLabel (LangString["IDC_PREF_UNMOUNT_SESSION_LOCKED"]); DismountOnLogOffCheckBox->SetLabel (LangString["LINUX_VC_QUITS"]); OpenExplorerWindowAfterMountCheckBox->SetLabel (LangString["LINUX_OPEN_FINDER"]); diff --git a/src/Main/Forms/TrueCrypt.fbp b/src/Main/Forms/TrueCrypt.fbp index a100bf1f74..6c457935e9 100644 --- a/src/Main/Forms/TrueCrypt.fbp +++ b/src/Main/Forms/TrueCrypt.fbp @@ -11134,7 +11134,7 @@ 0 0 wxID_ANY - LINUX_ENTERING_POVERSAWING + LINUX_ENTERING_POWERSAVING 0 diff --git a/src/Main/GraphicUserInterface.cpp b/src/Main/GraphicUserInterface.cpp index 321a939627..cf38325a43 100644 --- a/src/Main/GraphicUserInterface.cpp +++ b/src/Main/GraphicUserInterface.cpp @@ -30,6 +30,7 @@ #include "FatalErrorHandler.h" #ifdef TC_MACOSX #include "MacOSXSecureTextFieldHotkeys.h" +#include "MacOSXSleepLock.h" #endif #include "Forms/DeviceSelectionDialog.h" #include "Forms/KeyfileGeneratorDialog.h" @@ -184,6 +185,7 @@ namespace VeraCrypt { #ifdef TC_MACOSX UninstallMacOSXSecureTextFieldHotkeys(); + UninstallMacOSXSleepLockHandler(); #endif try { @@ -1038,6 +1040,38 @@ namespace VeraCrypt } } +#ifdef TC_MACOSX + // Called from MacOSXSleepLock.mm on the main thread when the system is about + // to sleep. wxWidgets does not deliver wxEVT_POWER_SUSPENDING on macOS + // (wxHAS_POWER_EVENTS is undefined there), so this observer fills that gap. + // AutoDismountVolumes is called with alwaysForce=false so that the + // ForceAutoDismount preference decides whether volumes with open files are + // detached, matching the Windows auto-dismount events (bForceAutoDismount). + void OnMacOSXSystemWillSleep () + { + if (Gui && Gui->GetPreferences().BackgroundTaskEnabled && Gui->GetPreferences().DismountOnPowerSaving) + { + VolumeInfoList mountedVolumes = Core->GetMountedVolumes(); + if (!mountedVolumes.empty()) + Gui->AutoDismountVolumes (mountedVolumes, false); + } + } + + // Called from MacOSXSleepLock.mm on the main thread when the user session + // has been verified as locked. macOS has no equivalent of the Windows + // screen-saver polling used by MainFrame::OnTimer, so screen-lock dismount + // is wired up here instead. Force semantics as above. + void OnMacOSXScreenLocked () + { + if (Gui && Gui->GetPreferences().BackgroundTaskEnabled && Gui->GetPreferences().DismountOnScreenSaver) + { + VolumeInfoList mountedVolumes = Core->GetMountedVolumes(); + if (!mountedVolumes.empty()) + Gui->AutoDismountVolumes (mountedVolumes, false); + } + } +#endif + bool GraphicUserInterface::OnInit () { Gui = this; @@ -1193,6 +1227,11 @@ namespace VeraCrypt #ifdef wxHAS_POWER_EVENTS Gui->Connect (wxEVT_POWER_SUSPENDING, wxPowerEventHandler (GraphicUserInterface::OnPowerSuspending)); #endif +#ifdef TC_MACOSX + // macOS lacks wxHAS_POWER_EVENTS; use native observers for sleep and + // screen lock so volumes can be auto-dismounted on those events. + InstallMacOSXSleepLockHandler(); +#endif mMainFrame = new MainFrame (nullptr); #if defined(TC_UNIX) diff --git a/src/Main/MacOSXSleepLock.h b/src/Main/MacOSXSleepLock.h new file mode 100644 index 0000000000..28e634e18c --- /dev/null +++ b/src/Main/MacOSXSleepLock.h @@ -0,0 +1,28 @@ +/* + Copyright (c) 2013-2026 AM Crypto. All rights reserved. + + Governed by the Apache License 2.0 the full text of which is + contained in the file License.txt included in VeraCrypt binary and source + code distribution packages. +*/ + +#ifndef TC_HEADER_Main_MacOSXSleepLock +#define TC_HEADER_Main_MacOSXSleepLock + +#ifdef TC_MACOSX +namespace VeraCrypt +{ + // Register/unregister observers for system sleep and screen lock. + // Implemented in MacOSXSleepLock.mm. + void InstallMacOSXSleepLockHandler (); + void UninstallMacOSXSleepLockHandler (); + + // Invoked by the observers above when the corresponding event occurs. + // Implemented on the C++ side (GraphicUserInterface.cpp) so that all + // wxWidgets / Core logic stays out of the Objective-C++ file. + void OnMacOSXSystemWillSleep (); + void OnMacOSXScreenLocked (); +} +#endif + +#endif // TC_HEADER_Main_MacOSXSleepLock diff --git a/src/Main/MacOSXSleepLock.mm b/src/Main/MacOSXSleepLock.mm new file mode 100644 index 0000000000..40ea53b7e6 --- /dev/null +++ b/src/Main/MacOSXSleepLock.mm @@ -0,0 +1,133 @@ +/* + Copyright (c) 2013-2026 AM Crypto. All rights reserved. + + Governed by the Apache License 2.0 the full text of which is + contained in the file License.txt included in VeraCrypt binary and source + code distribution packages. +*/ + +#include "System.h" +#include "MacOSXSleepLock.h" + +#ifdef TC_MACOSX +#import +#import + +// Queries the window server for the actual lock state of the current session. +// The com.apple.screenIsLocked distributed notification can be posted by any +// local process, so it is treated only as a hint and the dismount is performed +// only if the session really is locked. +static bool SessionIsLocked () +{ + bool locked = false; + CFDictionaryRef session = CGSessionCopyCurrentDictionary (); + if (session) + { + CFTypeRef value = CFDictionaryGetValue (session, CFSTR ("CGSSessionScreenIsLocked")); + locked = value && CFGetTypeID (value) == CFBooleanGetTypeID () && CFBooleanGetValue ((CFBooleanRef) value); + CFRelease (session); + } + return locked; +} + +@interface VCSleepLockObserver : NSObject +- (void) systemWillSleep: (NSNotification *) notification; +- (void) screenLocked: (NSNotification *) notification; +- (void) dismountIfSessionLocked; +@end + +@implementation VCSleepLockObserver + +// The callbacks below are entered from AppKit notification dispatch; a C++ +// exception must not unwind through those Objective-C frames, so every call +// into VeraCrypt code is guarded here. The dismount is best-effort: showing +// UI is not reliable while the session is locking or the system is going to +// sleep, so failures are ignored. + +- (void) systemWillSleep: (NSNotification *) notification +{ + (void) notification; + try + { + VeraCrypt::OnMacOSXSystemWillSleep (); + } + catch (...) { } +} + +- (void) screenLocked: (NSNotification *) notification +{ + (void) notification; + // The window server can update the session dictionary slightly after the + // notification is delivered, so if the session does not report itself as + // locked yet, re-check once after a short delay instead of giving up. + if (SessionIsLocked ()) + { + [self dismountIfSessionLocked]; + } + else + { + [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector (dismountIfSessionLocked) object: nil]; + [self performSelector: @selector (dismountIfSessionLocked) withObject: nil afterDelay: 1.0]; + } +} + +- (void) dismountIfSessionLocked +{ + if (!SessionIsLocked ()) + return; + + try + { + VeraCrypt::OnMacOSXScreenLocked (); + } + catch (...) { } +} + +@end + +namespace VeraCrypt +{ + // Non-ARC build (see the .mm compile rule in Build/Include/Makefile.inc), + // so the observer is retained by alloc/init and released in Uninstall. + static VCSleepLockObserver *SleepLockObserver = nil; + + void InstallMacOSXSleepLockHandler () + { + if (SleepLockObserver) + return; + + SleepLockObserver = [[VCSleepLockObserver alloc] init]; + + // System sleep (lid close, idle sleep, Apple menu > Sleep). Delivered on + // the main thread by NSWorkspace's own notification center, which only + // the system feeds (it is per-process, unlike the distributed center). + [[[NSWorkspace sharedWorkspace] notificationCenter] + addObserver: SleepLockObserver + selector: @selector (systemWillSleep:) + name: NSWorkspaceWillSleepNotification + object: nil]; + + // Screen lock. macOS exposes no public notification for this; the + // com.apple.screenIsLocked distributed notification is the long-standing + // de-facto mechanism used to detect it. It is untrusted input: the + // handler verifies the actual session lock state before dismounting. + [[NSDistributedNotificationCenter defaultCenter] + addObserver: SleepLockObserver + selector: @selector (screenLocked:) + name: @"com.apple.screenIsLocked" + object: nil]; + } + + void UninstallMacOSXSleepLockHandler () + { + if (!SleepLockObserver) + return; + + [NSObject cancelPreviousPerformRequestsWithTarget: SleepLockObserver]; + [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver: SleepLockObserver]; + [[NSDistributedNotificationCenter defaultCenter] removeObserver: SleepLockObserver]; + [SleepLockObserver release]; + SleepLockObserver = nil; + } +} +#endif diff --git a/src/Main/Main.make b/src/Main/Main.make index 36b8c1f6e9..ae9fdc37c2 100755 --- a/src/Main/Main.make +++ b/src/Main/Main.make @@ -28,6 +28,7 @@ OBJS += FatalErrorHandler.o OBJS += GraphicUserInterface.o ifeq "$(PLATFORM)" "MacOSX" OBJS += MacOSXSecureTextFieldHotkeys.o +OBJS += MacOSXSleepLock.o endif OBJS += VolumeHistory.o OBJS += Forms/AboutDialog.o