From 0c642cd614044c5ef30fc5127a76a50e6d938a39 Mon Sep 17 00:00:00 2001 From: Damian Rickard Date: Tue, 7 Jul 2026 21:04:51 -0400 Subject: [PATCH] macOS: auto-dismount volumes on sleep and screen lock wxWidgets does not expose power-suspend events on macOS, and the existing screen-saver polling path is Windows-only. Observe NSWorkspace sleep notifications and the long-standing distributed lock-state hints after the GUI is initialized, then route verified events through the existing auto-dismount preferences. Treat distributed notifications only as hints: request immediate delivery, verify the tri-state CGSession lock state, observe unlock transitions, and reconcile it from the existing two-second background timer. A sleep event honors either the power-saving or session-lock preference so lid-close sleep cannot bypass a lock-only configuration. Use a UI-free per-volume dismount path that preserves ForceAutoDismount, continues after failures, logs process status and error output, and performs credential cleanup conservatively after partial macOS dismounts. Security-event enumeration gives transient FUSE-T control files a shared monotonic retry deadline and logs every unresolved control path with its actual attempt count, while ordinary enumeration remains single-attempt. Expose and relabel the existing macOS preference controls. Sleep handling remains synchronous and best-effort within the NSWorkspace notification window; this change does not add an IOKit power assertion or claim to delay system sleep. --- src/Core/CoreBase.h | 2 +- src/Core/Unix/CoreUnix.cpp | 192 ++++++++++++++++++++------- src/Core/Unix/CoreUnix.h | 2 +- src/Main/Forms/Forms.cpp | 2 +- src/Main/Forms/MainFrame.cpp | 5 + src/Main/Forms/PreferencesDialog.cpp | 8 +- src/Main/Forms/TrueCrypt.fbp | 2 +- src/Main/GraphicUserInterface.cpp | 154 +++++++++++++++++++++ src/Main/GraphicUserInterface.h | 1 + src/Main/MacOSXSleepLock.h | 29 ++++ src/Main/MacOSXSleepLock.mm | 186 ++++++++++++++++++++++++++ src/Main/Main.make | 1 + 12 files changed, 532 insertions(+), 52 deletions(-) create mode 100644 src/Main/MacOSXSleepLock.h create mode 100644 src/Main/MacOSXSleepLock.mm diff --git a/src/Core/CoreBase.h b/src/Core/CoreBase.h index d76d40327b..1a4502aa37 100644 --- a/src/Core/CoreBase.h +++ b/src/Core/CoreBase.h @@ -57,7 +57,7 @@ namespace VeraCrypt virtual int GetOSMinorVersion () const = 0; virtual shared_ptr GetMountedVolume (const VolumePath &volumePath) const; virtual shared_ptr GetMountedVolume (VolumeSlotNumber slot) const; - virtual VolumeInfoList GetMountedVolumes (const VolumePath &volumePath = VolumePath()) const = 0; + virtual VolumeInfoList GetMountedVolumes (const VolumePath &volumePath = VolumePath(), bool retryControlFileAccess = false) const = 0; virtual bool HasAdminPrivileges () const = 0; virtual void Init () { } virtual bool IsDeviceChangeInProgress () const { return DeviceChangeInProgress; } diff --git a/src/Core/Unix/CoreUnix.cpp b/src/Core/Unix/CoreUnix.cpp index fee1eafb72..9fb554f94d 100644 --- a/src/Core/Unix/CoreUnix.cpp +++ b/src/Core/Unix/CoreUnix.cpp @@ -12,6 +12,9 @@ #include "CoreUnix.h" #include "Common/Tcdefs.h" +#ifdef VC_MACOSX_FUSET +#include +#endif #include #include #include @@ -521,76 +524,173 @@ namespace VeraCrypt return mountedFilesystems.front()->MountPoint; } - VolumeInfoList CoreUnix::GetMountedVolumes (const VolumePath &volumePath) const + static shared_ptr ReadMountedVolumeControlFile (const MountedFilesystem &mountedFileSystem, string *controlFileError) { - VolumeInfoList volumes; + if (controlFileError) + controlFileError->clear(); - foreach_ref (const MountedFilesystem &mf, GetMountedFilesystems ()) + try { - if (string (mf.MountPoint).find (GetFuseMountDirPrefix()) == string::npos) - continue; + shared_ptr controlFile (new File); + controlFile->Open (string (mountedFileSystem.MountPoint) + FuseService::GetControlPath()); - shared_ptr mountedVol; - // Introduce a retry mechanism with a timeout for control file access. - // The list is already filtered to VeraCrypt auxiliary mounts; in - // FUSE-T builds, the mount table device name varies by backend. + FileStream controlFileReader (controlFile); + string controlFileData = controlFileReader.ReadToEnd(); + if (controlFileData.empty() || controlFileData.size() > 1024 * 1024) + throw ParameterIncorrect (SRC_POS); + + shared_ptr controlFileStream (new MemoryStream (ConstBufferPtr ((const uint8 *) controlFileData.data(), controlFileData.size()))); + return Serializable::DeserializeNew (controlFileStream); + } + catch (const std::exception &e) + { #ifdef VC_MACOSX_FUSET - int controlFileRetries = volumePath.IsEmpty() ? 1 : 10; // Up to 10 attempts with 500ms sleeps for specific volume lookups - string controlFileError; - while (!mountedVol && (controlFileRetries-- > 0)) + if (controlFileError) + *controlFileError = StringConverter::ToSingle (StringConverter::ToExceptionString (e)); +#else + (void) e; #endif + } +#ifdef VC_MACOSX_FUSET + catch (...) + { + if (controlFileError) + *controlFileError = "unknown exception"; + } +#endif + + return shared_ptr (); + } + + VolumeInfoList CoreUnix::GetMountedVolumes (const VolumePath &volumePath, bool retryControlFileAccess) const + { + VolumeInfoList volumes; + + struct MountedVolumeCandidate + { + MountedVolumeCandidate (shared_ptr mountedFileSystem) + : FileSystem (mountedFileSystem), ControlFileAttempts (0) { } + + shared_ptr FileSystem; + shared_ptr Volume; + string ControlFileError; + int ControlFileAttempts; + }; + + vector candidates; + MountedFilesystemList mountedFileSystems = GetMountedFilesystems (); + for (MountedFilesystemList::const_iterator i = mountedFileSystems.begin(); i != mountedFileSystems.end(); ++i) + { + if (string ((*i)->MountPoint).find (GetFuseMountDirPrefix()) != string::npos) + candidates.push_back (MountedVolumeCandidate (*i)); + } + +#ifdef VC_MACOSX_FUSET + if (!volumePath.IsEmpty()) + { + // Preserve the existing per-candidate retry behavior for specific + // volume lookups while a FUSE-T auxiliary mount becomes readable. + for (size_t candidateIndex = 0; candidateIndex < candidates.size(); ++candidateIndex) { - try + MountedVolumeCandidate &candidate = candidates[candidateIndex]; + for (int attempt = 0; attempt < 10 && !candidate.Volume; ++attempt) { - shared_ptr controlFile (new File); - controlFile->Open (string (mf.MountPoint) + FuseService::GetControlPath()); + ++candidate.ControlFileAttempts; + candidate.Volume = ReadMountedVolumeControlFile (*candidate.FileSystem, &candidate.ControlFileError); + if (!candidate.Volume && attempt + 1 < 10) + Thread::Sleep (500); + } - FileStream controlFileReader (controlFile); - string controlFileData = controlFileReader.ReadToEnd(); - if (controlFileData.empty() || controlFileData.size() > 1024 * 1024) - throw ParameterIncorrect (SRC_POS); + if (candidate.Volume && wstring (candidate.Volume->Path).compare (volumePath) == 0) + break; + } + } + else + { + const bool useSecurityRetryDeadline = retryControlFileAccess; + const std::chrono::steady_clock::time_point retryDeadline = + std::chrono::steady_clock::now() + std::chrono::seconds (2); + + // Every candidate receives one initial attempt. Security-event callers + // then retry unresolved control files in shared, rotating rounds; no + // retry read or sleep begins after the monotonic deadline. + for (size_t candidateIndex = 0; candidateIndex < candidates.size(); ++candidateIndex) + { + MountedVolumeCandidate &candidate = candidates[candidateIndex]; + ++candidate.ControlFileAttempts; + candidate.Volume = ReadMountedVolumeControlFile (*candidate.FileSystem, &candidate.ControlFileError); + } - shared_ptr controlFileStream (new MemoryStream (ConstBufferPtr ((const uint8 *) controlFileData.data(), controlFileData.size()))); - mountedVol = Serializable::DeserializeNew (controlFileStream); - } - catch (const std::exception& e) + size_t retryStartIndex = 0; + while (useSecurityRetryDeadline && !candidates.empty()) + { + bool retryableCandidateFound = false; + for (size_t candidateIndex = 0; candidateIndex < candidates.size(); ++candidateIndex) { -#ifdef VC_MACOSX_FUSET - controlFileError = StringConverter::ToSingle (StringConverter::ToExceptionString (e)); - if (controlFileRetries > 0) + if (!candidates[candidateIndex].Volume && candidates[candidateIndex].ControlFileAttempts < 10) { - // FUSE-T's SMB backend can briefly expose the auxiliary mount - // before the control file is readable and deserializable. - Thread::Sleep (500); + retryableCandidateFound = true; + break; } -#else - (void) e; -#endif } -#ifdef VC_MACOSX_FUSET - catch (...) + + if (!retryableCandidateFound) + break; + + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + if (now >= retryDeadline) + break; + + std::chrono::milliseconds remaining = + std::chrono::duration_cast (retryDeadline - now); + uint32 retryDelay = remaining.count() > 200 ? 200 : static_cast (remaining.count()); + if (retryDelay > 0) + Thread::Sleep (retryDelay); + + for (size_t offset = 0; offset < candidates.size(); ++offset) { - controlFileError = "unknown exception"; - if (controlFileRetries > 0) - { - // FUSE-T's SMB backend can briefly expose the auxiliary mount - // before the control file is readable and deserializable. - Thread::Sleep (500); - } + if (std::chrono::steady_clock::now() >= retryDeadline) + break; + + size_t candidateIndex = (retryStartIndex + offset) % candidates.size(); + MountedVolumeCandidate &candidate = candidates[candidateIndex]; + if (candidate.Volume || candidate.ControlFileAttempts >= 10) + continue; + + ++candidate.ControlFileAttempts; + candidate.Volume = ReadMountedVolumeControlFile (*candidate.FileSystem, &candidate.ControlFileError); } -#endif + + retryStartIndex = (retryStartIndex + 1) % candidates.size(); } + } +#else + (void) retryControlFileAccess; + for (size_t candidateIndex = 0; candidateIndex < candidates.size(); ++candidateIndex) + { + MountedVolumeCandidate &candidate = candidates[candidateIndex]; + ++candidate.ControlFileAttempts; + candidate.Volume = ReadMountedVolumeControlFile (*candidate.FileSystem, nullptr); + } +#endif + + for (size_t candidateIndex = 0; candidateIndex < candidates.size(); ++candidateIndex) + { + MountedVolumeCandidate &candidate = candidates[candidateIndex]; + const MountedFilesystem &mf = *candidate.FileSystem; + shared_ptr mountedVol = candidate.Volume; if (!mountedVol) { #ifdef VC_MACOSX_FUSET - if (!volumePath.IsEmpty()) + if (candidate.ControlFileAttempts > 0 && (retryControlFileAccess || !volumePath.IsEmpty())) { stringstream logMessage; - logMessage << "Failed to read VeraCrypt auxiliary mount control file after retries: " + logMessage << "Failed to read VeraCrypt auxiliary mount control file after " + << candidate.ControlFileAttempts << " attempt(s): " << string (mf.MountPoint) << FuseService::GetControlPath(); - if (!controlFileError.empty()) - logMessage << ": " << controlFileError; + if (!candidate.ControlFileError.empty()) + logMessage << ": " << candidate.ControlFileError; SystemLog::WriteError (logMessage.str()); } #endif diff --git a/src/Core/Unix/CoreUnix.h b/src/Core/Unix/CoreUnix.h index a48dfbd6b0..e70987d86f 100644 --- a/src/Core/Unix/CoreUnix.h +++ b/src/Core/Unix/CoreUnix.h @@ -38,7 +38,7 @@ namespace VeraCrypt virtual uint64 GetDeviceSize (const DevicePath &devicePath) const; virtual int GetOSMajorVersion () const { throw NotApplicable (SRC_POS); } virtual int GetOSMinorVersion () const { throw NotApplicable (SRC_POS); } - virtual VolumeInfoList GetMountedVolumes (const VolumePath &volumePath = VolumePath()) const; + virtual VolumeInfoList GetMountedVolumes (const VolumePath &volumePath = VolumePath(), bool retryControlFileAccess = false) const; virtual bool IsDevicePresent (const DevicePath &device) const { throw NotApplicable (SRC_POS); } virtual bool IsInPortableMode () const { return false; } virtual bool IsMountPointAvailable (const DirectoryPath &mountPoint) const; 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/MainFrame.cpp b/src/Main/Forms/MainFrame.cpp index ce6d0eddd4..909707ce90 100644 --- a/src/Main/Forms/MainFrame.cpp +++ b/src/Main/Forms/MainFrame.cpp @@ -24,6 +24,9 @@ #include "Main/Resources.h" #include "Main/Application.h" #include "Main/GraphicUserInterface.h" +#ifdef TC_MACOSX +#include "Main/MacOSXSleepLock.h" +#endif #include "Main/VolumeHistory.h" #include "Main/Xml.h" #include "MainFrame.h" @@ -1435,6 +1438,8 @@ namespace VeraCrypt previousState = running; } } +#elif defined (TC_MACOSX) + ReconcileMacOSXScreenLockState (); #endif } } 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..9e3205693b 100644 --- a/src/Main/GraphicUserInterface.cpp +++ b/src/Main/GraphicUserInterface.cpp @@ -30,6 +30,8 @@ #include "FatalErrorHandler.h" #ifdef TC_MACOSX #include "MacOSXSecureTextFieldHotkeys.h" +#include "MacOSXSleepLock.h" +#include "Platform/SystemLog.h" #endif #include "Forms/DeviceSelectionDialog.h" #include "Forms/KeyfileGeneratorDialog.h" @@ -184,6 +186,7 @@ namespace VeraCrypt { #ifdef TC_MACOSX UninstallMacOSXSecureTextFieldHotkeys(); + UninstallMacOSXSleepLockHandler(); #endif try { @@ -1038,6 +1041,152 @@ namespace VeraCrypt } } +#ifdef TC_MACOSX + static void LogMacOSXAutoDismountError (const char *eventName, shared_ptr volume, const exception *ex) + { + try + { + stringstream message; + message << "macOS auto-dismount during " << eventName; + if (volume) + message << " for volume " << StringConverter::ToSingle (wstring (volume->Path)); + message << " failed"; + if (ex) + { + const ExecutedProcessFailed *processException = dynamic_cast (ex); + if (processException) + { + message << ": command " << processException->GetCommand() + << " exited with status " << processException->GetExitCode(); + string errorOutput = StringConverter::Trim (processException->GetErrorOutput()); + if (!errorOutput.empty()) + message << ": " << errorOutput; + } + else + message << ": " << StringConverter::ToSingle (StringConverter::ToExceptionString (*ex)); + } + else + message << ": unknown exception"; + SystemLog::WriteError (message.str()); + } + catch (...) { } + } + + void GraphicUserInterface::AutoDismountVolumesForMacOSXSecurityEvent (const char *eventName) + { + try + { + UserPreferences preferences = GetPreferences (); + VolumeInfoList mountedVolumes = Core->GetMountedVolumes (VolumePath(), true); + mountedVolumes.sort (VolumeInfo::FirstVolumeMountedAfterSecond); + + bool securityCleanupRequired = false; + foreach (shared_ptr volume, mountedVolumes) + { + try + { + Core->DismountVolume (volume, preferences.ForceAutoDismount); + securityCleanupRequired = true; + } + catch (MountedVolumeInUse &e) + { + LogMacOSXAutoDismountError (eventName, volume, &e); + } + catch (exception &e) + { + // On macOS the user-visible device can be detached before + // auxiliary-mount cleanup throws. Wipe cached credentials and + // close token sessions conservatively after such failures. + securityCleanupRequired = true; + LogMacOSXAutoDismountError (eventName, volume, &e); + } + catch (...) + { + securityCleanupRequired = true; + LogMacOSXAutoDismountError (eventName, volume, nullptr); + } + } + + if (securityCleanupRequired) + { + try + { + OnVolumesAutoDismounted (); + } + catch (exception &e) + { + LogMacOSXAutoDismountError (eventName, shared_ptr (), &e); + } + catch (...) + { + LogMacOSXAutoDismountError (eventName, shared_ptr (), nullptr); + } + } + } + catch (exception &e) + { + LogMacOSXAutoDismountError (eventName, shared_ptr (), &e); + } + catch (...) + { + LogMacOSXAutoDismountError (eventName, shared_ptr (), nullptr); + } + } + + // 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. + // The lock preference also applies here because a lid-close sleep can suspend + // the run loop before the separate screen-lock notification is delivered. + void OnMacOSXSystemWillSleep () + { + try + { + if (Gui) + { + UserPreferences preferences = Gui->GetPreferences (); + if (preferences.BackgroundTaskEnabled + && (preferences.DismountOnPowerSaving || preferences.DismountOnScreenSaver)) + { + Gui->AutoDismountVolumesForMacOSXSecurityEvent ("system sleep"); + } + } + } + catch (exception &e) + { + LogMacOSXAutoDismountError ("system sleep", shared_ptr (), &e); + } + catch (...) + { + LogMacOSXAutoDismountError ("system sleep", shared_ptr (), nullptr); + } + } + + // Called only after MacOSXSleepLock.mm verifies a transition to the locked + // state. The dedicated security-event path honors ForceAutoDismount and does + // not display UI while the session is locked. + void OnMacOSXScreenLocked () + { + try + { + if (Gui) + { + UserPreferences preferences = Gui->GetPreferences (); + if (preferences.BackgroundTaskEnabled && preferences.DismountOnScreenSaver) + Gui->AutoDismountVolumesForMacOSXSecurityEvent ("screen lock"); + } + } + catch (exception &e) + { + LogMacOSXAutoDismountError ("screen lock", shared_ptr (), &e); + } + catch (...) + { + LogMacOSXAutoDismountError ("screen lock", shared_ptr (), nullptr); + } + } +#endif + bool GraphicUserInterface::OnInit () { Gui = this; @@ -1193,6 +1342,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/GraphicUserInterface.h b/src/Main/GraphicUserInterface.h index fba52b19a5..cb13a79897 100644 --- a/src/Main/GraphicUserInterface.h +++ b/src/Main/GraphicUserInterface.h @@ -101,6 +101,7 @@ namespace VeraCrypt void ExecuteWaitThreadRoutine (wxWindow *parent, WaitThreadRoutine *pRoutine) const; #ifdef TC_MACOSX + void AutoDismountVolumesForMacOSXSecurityEvent (const char *eventName); virtual void MacOpenFiles (const wxArrayString &fileNames); virtual void MacReopenApp (); static bool HandlePasswordEntryCustomEvent (wxEvent& event); diff --git a/src/Main/MacOSXSleepLock.h b/src/Main/MacOSXSleepLock.h new file mode 100644 index 0000000000..ef7b8822d6 --- /dev/null +++ b/src/Main/MacOSXSleepLock.h @@ -0,0 +1,29 @@ +/* + 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 (); + void ReconcileMacOSXScreenLockState (); + + // Invoked after a trusted sleep notification or a verified screen-lock + // transition. 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..6496c40bc5 --- /dev/null +++ b/src/Main/MacOSXSleepLock.mm @@ -0,0 +1,186 @@ +/* + 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" +#include "Platform/SystemLog.h" + +#ifdef TC_MACOSX +#import +#import + +enum VCSessionLockState +{ + VCSessionLockStateUnknown, + VCSessionLockStateUnlocked, + VCSessionLockStateLocked +}; + +static VCSessionLockState LastSessionLockState = VCSessionLockStateUnknown; + +static void LogSleepLockError (const char *context, const std::exception *ex) +{ + try + { + std::stringstream message; + message << "macOS " << context << " callback failed"; + if (ex) + message << ": " << VeraCrypt::StringConverter::ToSingle (VeraCrypt::StringConverter::ToExceptionString (*ex)); + else + message << ": unknown exception"; + VeraCrypt::SystemLog::WriteError (message.str()); + } + catch (...) { } +} + +// Distributed lock notifications are untrusted hints. Query the window server +// for the actual state and preserve Unknown separately from Unlocked so a +// transient query failure cannot create a false transition. +static VCSessionLockState GetSessionLockState () +{ + CFDictionaryRef session = CGSessionCopyCurrentDictionary (); + if (!session) + return VCSessionLockStateUnknown; + + CFTypeRef value = CFDictionaryGetValue (session, CFSTR ("CGSSessionScreenIsLocked")); + // The private lock key is absent from a valid dictionary while the session + // is unlocked. Preserve Unknown only for a query failure or malformed value. + VCSessionLockState state = VCSessionLockStateUnlocked; + if (value) + state = CFGetTypeID (value) == CFBooleanGetTypeID () + ? (CFBooleanGetValue ((CFBooleanRef) value) ? VCSessionLockStateLocked : VCSessionLockStateUnlocked) + : VCSessionLockStateUnknown; + + CFRelease (session); + return state; +} + +@interface VCSleepLockObserver : NSObject +- (void) systemWillSleep: (NSNotification *) notification; +- (void) sessionStateChanged: (NSNotification *) notification; +@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 and failures are logged without displaying UI. + +- (void) systemWillSleep: (NSNotification *) notification +{ + (void) notification; + try + { + VeraCrypt::OnMacOSXSystemWillSleep (); + } + catch (const std::exception &e) + { + LogSleepLockError ("system sleep", &e); + } + catch (...) + { + LogSleepLockError ("system sleep", nullptr); + } +} + +- (void) sessionStateChanged: (NSNotification *) notification +{ + (void) notification; + try + { + VeraCrypt::ReconcileMacOSXScreenLockState (); + } + catch (const std::exception &e) + { + LogSleepLockError ("session state", &e); + } + catch (...) + { + LogSleepLockError ("session state", nullptr); + } +} + +@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 ReconcileMacOSXScreenLockState () + { + try + { + VCSessionLockState state = GetSessionLockState (); + if (state == VCSessionLockStateUnknown) + return; + + bool enteredLockedState = state == VCSessionLockStateLocked && LastSessionLockState != VCSessionLockStateLocked; + LastSessionLockState = state; + if (enteredLockedState) + OnMacOSXScreenLocked (); + } + catch (const std::exception &e) + { + LogSleepLockError ("session reconciliation", &e); + } + catch (...) + { + LogSleepLockError ("session reconciliation", nullptr); + } + } + + void InstallMacOSXSleepLockHandler () + { + if (SleepLockObserver) + return; + + SleepLockObserver = [[VCSleepLockObserver alloc] init]; + LastSessionLockState = VCSessionLockStateUnknown; + + // System sleep (lid close, idle sleep, Apple menu > Sleep). NSWorkspace's + // notification center is process-local, so external local processes cannot + // inject this notification as they can with the distributed center below. + [[[NSWorkspace sharedWorkspace] notificationCenter] + addObserver: SleepLockObserver + selector: @selector (systemWillSleep:) + name: NSWorkspaceWillSleepNotification + object: nil]; + + // macOS exposes no public screen-lock notification. These long-standing + // distributed notifications are untrusted hints, so both lock and unlock + // handlers verify the actual session state. DeliverImmediately prevents + // notification coalescing while VeraCrypt is inactive or hidden. + [[NSDistributedNotificationCenter defaultCenter] + addObserver: SleepLockObserver + selector: @selector (sessionStateChanged:) + name: @"com.apple.screenIsLocked" + object: nil + suspensionBehavior: NSNotificationSuspensionBehaviorDeliverImmediately]; + [[NSDistributedNotificationCenter defaultCenter] + addObserver: SleepLockObserver + selector: @selector (sessionStateChanged:) + name: @"com.apple.screenIsUnlocked" + object: nil + suspensionBehavior: NSNotificationSuspensionBehaviorDeliverImmediately]; + } + + void UninstallMacOSXSleepLockHandler () + { + if (!SleepLockObserver) + return; + + [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver: SleepLockObserver]; + [[NSDistributedNotificationCenter defaultCenter] removeObserver: SleepLockObserver]; + [SleepLockObserver release]; + SleepLockObserver = nil; + LastSessionLockState = VCSessionLockStateUnknown; + } +} +#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