macOS: validate format wizard device targets

Keep device selection enumeration unchanged to avoid slow dialog loads.

In the format wizard, inspect only the selected target with diskutil info -plist and reject APFS synthesized devices, macOS system/support targets, read-only targets, and current APFS system stores. Add a read-only APFS hint for creation failures.
This commit is contained in:
Mounir IDRASSI
2026-05-11 15:04:17 +02:00
committed by Mounir IDRASSI
parent e6247fbf2a
commit 49c8fd3680
2 changed files with 353 additions and 0 deletions

View File

@@ -1515,6 +1515,11 @@
<entry lang="en" key="LINUX_ERROR_TRY_ENCRYPT_SYSTEM_PARTITION">Error: You are trying to encrypt a system partition.\n\nVeraCrypt can encrypt system partitions only under Windows.</entry>
<entry lang="en" key="LINUX_WARNING_FORMAT_DESTROY_FS">WARNING: Formatting of the device will destroy all data on filesystem '{0}'.\n\nDo you want to continue?</entry>
<entry lang="en" key="LINUX_MOUNTET_HINT">The filesystem of the selected device is currently mounted. Please unmount '{0}' before proceeding.</entry>
<entry lang="en" key="MACOSX_APFS_SYNTHESIZED_DEVICE">The selected device '{0}' is an APFS synthesized container or volume and cannot be used as a raw VeraCrypt volume host.\n\nSelect the physical APFS store partition{1} instead.</entry>
<entry lang="en" key="MACOSX_DEVICE_SYSTEM_PARTITION">The selected device '{0}' is a macOS system/support partition and cannot be used as a VeraCrypt volume host.</entry>
<entry lang="en" key="MACOSX_APFS_SYSTEM_STORE">The selected APFS physical store '{0}' contains the currently mounted macOS system volume and cannot be used as a VeraCrypt volume host.</entry>
<entry lang="en" key="MACOSX_DEVICE_NOT_WRITABLE">macOS reports the selected device '{0}' as read-only. Select a writable physical partition or disk.</entry>
<entry lang="en" key="MACOSX_APFS_EROFS_HINT">macOS reported the selected device as read-only. If this is an APFS disk, make sure you selected the physical APFS store partition, not an APFS synthesized volume. Use Disk Utility or 'diskutil list' to identify the physical partition, then retry.</entry>
<entry lang="en" key="LINUX_HIDDEN_PASS_NO_DIFF">The Hidden volume can't have the same password, PIM and keyfiles as the Outer volume</entry>
<entry lang="en" key="LINUX_NOT_FAT_HINT">Please note that the volume will not be formatted with a FAT filesystem and, therefore, you may be required to install additional filesystem drivers on platforms other than {0}, which will enable you to mount the volume.</entry>
<entry lang="en" key="LINUX_ERROR_SIZE_HIDDEN_VOL">Error: The hidden volume to be created is larger than {0} TB ({1} GB).\n\nPossible solutions:\n- Create a container/partition smaller than {0} TB.\n</entry>

View File

@@ -13,6 +13,7 @@
#include "System.h"
#include "Platform/SystemInfo.h"
#ifdef TC_UNIX
#include <errno.h>
#include <unistd.h>
#include <sys/statvfs.h> // header for statvfs
#include "Platform/Unix/Process.h"
@@ -52,6 +53,340 @@ namespace VeraCrypt
#ifdef TC_MACOSX
static string DecodeMacOSXPlistXmlString (const string &xmlString)
{
string decoded;
for (size_t i = 0; i < xmlString.size(); ++i)
{
if (xmlString[i] != '&')
{
decoded += xmlString[i];
continue;
}
if (xmlString.compare (i, 5, "&amp;") == 0)
{
decoded += '&';
i += 4;
}
else if (xmlString.compare (i, 4, "&lt;") == 0)
{
decoded += '<';
i += 3;
}
else if (xmlString.compare (i, 4, "&gt;") == 0)
{
decoded += '>';
i += 3;
}
else if (xmlString.compare (i, 6, "&quot;") == 0)
{
decoded += '"';
i += 5;
}
else if (xmlString.compare (i, 6, "&apos;") == 0)
{
decoded += '\'';
i += 5;
}
else
decoded += xmlString[i];
}
return decoded;
}
static bool ExtractMacOSXPlistString (const string &xml, const string &key, string &value)
{
string keyTag = "<key>" + key + "</key>";
size_t p = xml.find (keyTag);
if (p == string::npos)
return false;
p = xml.find ("<string>", p + keyTag.size());
if (p == string::npos)
return false;
p += 8;
size_t e = xml.find ("</string>", p);
if (e == string::npos)
return false;
value = DecodeMacOSXPlistXmlString (xml.substr (p, e - p));
return true;
}
static bool ExtractMacOSXPlistBool (const string &xml, const string &key, bool &value)
{
string keyTag = "<key>" + key + "</key>";
size_t p = xml.find (keyTag);
if (p == string::npos)
return false;
p += keyTag.size();
size_t truePos = xml.find ("<true/>", p);
size_t falsePos = xml.find ("<false/>", p);
size_t nextKeyPos = xml.find ("<key>", p);
if (truePos != string::npos && (nextKeyPos == string::npos || truePos < nextKeyPos)
&& (falsePos == string::npos || truePos < falsePos))
{
value = true;
return true;
}
if (falsePos != string::npos && (nextKeyPos == string::npos || falsePos < nextKeyPos))
{
value = false;
return true;
}
return false;
}
static list <string> ExtractMacOSXAPFSPhysicalStores (const string &xml)
{
list <string> stores;
size_t arrayPos = xml.find ("<key>APFSPhysicalStores</key>");
if (arrayPos == string::npos)
return stores;
size_t arrayEnd = xml.find ("</array>", arrayPos);
if (arrayEnd == string::npos)
return stores;
for (size_t p = arrayPos; p < arrayEnd; )
{
size_t keyPos = xml.find ("<key>APFSPhysicalStore</key>", p);
size_t alternateKeyPos = xml.find ("<key>DeviceIdentifier</key>", p);
if (alternateKeyPos != string::npos && alternateKeyPos < arrayEnd
&& (keyPos == string::npos || alternateKeyPos < keyPos))
keyPos = alternateKeyPos;
if (keyPos == string::npos || keyPos >= arrayEnd)
break;
size_t stringPos = xml.find ("<string>", keyPos);
if (stringPos == string::npos || stringPos >= arrayEnd)
break;
stringPos += 8;
size_t stringEnd = xml.find ("</string>", stringPos);
if (stringEnd == string::npos || stringEnd > arrayEnd)
break;
stores.push_back (DecodeMacOSXPlistXmlString (xml.substr (stringPos, stringEnd - stringPos)));
p = stringEnd + 9;
}
return stores;
}
static string GetMacOSXDiskutilDevicePath (const VolumePath &devicePath)
{
string path = devicePath;
if (path.find ("/dev/rdisk") == 0)
path = string ("/dev/disk") + path.substr (10);
return path;
}
static string GetMacOSXRawDevicePath (const string &deviceIdentifier)
{
if (deviceIdentifier.find ("/dev/rdisk") == 0)
return deviceIdentifier;
if (deviceIdentifier.find ("/dev/disk") == 0)
return string ("/dev/r") + deviceIdentifier.substr (5);
if (deviceIdentifier.find ("disk") == 0)
return string ("/dev/r") + deviceIdentifier;
return deviceIdentifier;
}
static string GetMacOSXDiskutilInfo (const VolumePath &devicePath)
{
list <string> args;
args.push_back ("info");
args.push_back ("-plist");
args.push_back (GetMacOSXDiskutilDevicePath (devicePath));
return Process::Execute ("/usr/sbin/diskutil", args);
}
static bool IsMacOSXSystemSupportContent (const string &content)
{
string lowerContent = StringConverter::ToLower (content);
return lowerContent == "efi"
|| lowerContent == "apple_apfs_isc"
|| lowerContent == "apple_apfs_recovery"
|| lowerContent == "apple_boot"
|| lowerContent == "apple_partition_map";
}
static bool IsMacOSXSystemMountPoint (const string &mountPoint)
{
return mountPoint == "/" || mountPoint.find ("/System/Volumes/") == 0;
}
static bool IsMacOSXAPFSSynthesizedDevice (const string &infoXml)
{
string containerReference;
string filesystemType;
string virtualOrPhysical;
bool partitionMapPartition = false;
bool wholeDisk = false;
bool hasPartitionMapPartition = ExtractMacOSXPlistBool (infoXml, "PartitionMapPartition", partitionMapPartition);
bool hasWholeDisk = ExtractMacOSXPlistBool (infoXml, "WholeDisk", wholeDisk);
ExtractMacOSXPlistString (infoXml, "APFSContainerReference", containerReference);
ExtractMacOSXPlistString (infoXml, "FilesystemType", filesystemType);
ExtractMacOSXPlistString (infoXml, "VirtualOrPhysical", virtualOrPhysical);
if (StringConverter::ToLower (virtualOrPhysical) == "virtual" && !containerReference.empty())
return true;
if (!ExtractMacOSXAPFSPhysicalStores (infoXml).empty() && hasPartitionMapPartition && !partitionMapPartition)
return true;
if (StringConverter::ToLower (filesystemType) == "apfs"
&& hasPartitionMapPartition && !partitionMapPartition
&& hasWholeDisk && !wholeDisk)
{
return true;
}
return false;
}
static bool IsMacOSXDeviceReadOnly (const string &infoXml)
{
bool value = false;
// Writable and ReadOnlyVolume can reflect a read-only filesystem mount;
// only media writability is fatal before unmounting.
if (ExtractMacOSXPlistBool (infoXml, "ReadOnlyMedia", value) && value)
return true;
if (ExtractMacOSXPlistBool (infoXml, "WritableMedia", value) && !value)
return true;
return false;
}
static bool IsSelectedMacOSXSystemAPFSDevice (const string &selectedInfoXml, bool &wholeDiskSelected)
{
wholeDiskSelected = false;
string selectedDeviceIdentifier;
if (!ExtractMacOSXPlistString (selectedInfoXml, "DeviceIdentifier", selectedDeviceIdentifier) || selectedDeviceIdentifier.empty())
return false;
ExtractMacOSXPlistBool (selectedInfoXml, "WholeDisk", wholeDiskSelected);
try
{
string rootInfoXml = GetMacOSXDiskutilInfo (VolumePath (FilesystemPath ("/")));
list <string> rootStores = ExtractMacOSXAPFSPhysicalStores (rootInfoXml);
foreach (const string &store, rootStores)
{
if (selectedDeviceIdentifier == store)
return true;
if (wholeDiskSelected)
{
try
{
string storeInfoXml = GetMacOSXDiskutilInfo (VolumePath (FilesystemPath (GetMacOSXRawDevicePath (store))));
string storeParentWholeDisk;
if (ExtractMacOSXPlistString (storeInfoXml, "ParentWholeDisk", storeParentWholeDisk)
&& selectedDeviceIdentifier == storeParentWholeDisk)
{
return true;
}
}
catch (...) { }
}
}
}
catch (...) { }
return false;
}
static bool ValidateMacOSXSelectedDeviceForCreation (const VolumePath &devicePath)
{
string infoXml;
try
{
infoXml = GetMacOSXDiskutilInfo (devicePath);
}
catch (...)
{
return true;
}
if (IsMacOSXAPFSSynthesizedDevice (infoXml))
{
wstring recommendedPath;
list <string> stores = ExtractMacOSXAPFSPhysicalStores (infoXml);
if (!stores.empty())
recommendedPath = L" (" + StringConverter::ToWide (GetMacOSXRawDevicePath (stores.front())) + L")";
Gui->ShowError (StringFormatter (LangString["MACOSX_APFS_SYNTHESIZED_DEVICE"], wstring (devicePath), recommendedPath));
return false;
}
string content;
if (ExtractMacOSXPlistString (infoXml, "Content", content) && IsMacOSXSystemSupportContent (content))
{
Gui->ShowError (StringFormatter (LangString["MACOSX_DEVICE_SYSTEM_PARTITION"], wstring (devicePath)));
return false;
}
string mountPoint;
if (ExtractMacOSXPlistString (infoXml, "MountPoint", mountPoint) && IsMacOSXSystemMountPoint (mountPoint))
{
Gui->ShowError (LangString["LINUX_ERROR_TRY_ENCRYPT_SYSTEM_PARTITION"]);
return false;
}
bool selectedSystemWholeDisk = false;
if (IsSelectedMacOSXSystemAPFSDevice (infoXml, selectedSystemWholeDisk))
{
if (selectedSystemWholeDisk)
Gui->ShowError (LangString["LINUX_ERROR_TRY_ENCRYPT_SYSTEM_DRIVE"]);
else
Gui->ShowError (StringFormatter (LangString["MACOSX_APFS_SYSTEM_STORE"], wstring (devicePath)));
return false;
}
if (IsMacOSXDeviceReadOnly (infoXml))
{
Gui->ShowError (StringFormatter (LangString["MACOSX_DEVICE_NOT_WRITABLE"], wstring (devicePath)));
return false;
}
return true;
}
static void ShowMacOSXVolumeCreationError (const VolumePath &devicePath, const exception &e)
{
const SystemException *sysEx = dynamic_cast <const SystemException *> (&e);
if (devicePath.IsDevice() && sysEx && sysEx->GetErrorCode() == EROFS)
{
Gui->ShowError (UserInterface::ExceptionToMessage (e) + L"\n\n" + LangString["MACOSX_APFS_EROFS_HINT"]);
return;
}
Gui->ShowError (e);
}
bool VolumeCreationWizard::ProcessEvent(wxEvent& event)
{
if(GraphicUserInterface::HandlePasswordEntryCustomEvent (event))
@@ -590,7 +925,11 @@ namespace VeraCrypt
SetWorkInProgress (false);
workInProgressCleared = true;
}
#ifdef TC_MACOSX
ShowMacOSXVolumeCreationError (SelectedVolumePath, e);
#else
Gui->ShowError (e);
#endif
}
if (!workInProgressCleared)
@@ -672,6 +1011,11 @@ namespace VeraCrypt
DeviceWarningConfirmed = true;
#ifdef TC_MACOSX
if (!ValidateMacOSXSelectedDeviceForCreation (SelectedVolumePath))
return GetCurrentStep();
#endif
foreach_ref (const HostDevice &drive, Core->GetHostDevices())
{
if (drive.Path == SelectedVolumePath && !drive.Partitions.empty())
@@ -1108,7 +1452,11 @@ namespace VeraCrypt
{
CreationAborted = true;
OnVolumeCreatorFinished();
#ifdef TC_MACOSX
ShowMacOSXVolumeCreationError (SelectedVolumePath, e);
#else
Gui->ShowError (e);
#endif
}
}