mirror of
https://github.com/veracrypt/VeraCrypt.git
synced 2026-10-05 16:46:35 -05:00
macOS: harden FUSE-T dismount ownership and teardown
Keep FUSE serving until the auxiliary SMB filesystem is unmounted, then join the shutdown worker before destroying the FUSE handle. Authenticate socket peers and bind each request to the service and filesystem instance; carry the force flag through to unmount and reply before teardown. Resolve the current disk image before detach instead of trusting cached BSD device numbers. Clear device and mount metadata when the image is gone, and abort on inventory errors. Refresh ownership during enumeration and before filesystem checks. Give each auxiliary mount a random path so an old backend cannot target a subsequent VeraCrypt mount during cleanup. Canonicalize the auxiliary path before service startup and hdiutil attach. Resolve older clients' image paths through TMPDIR aliases without accessing unrelated images. Treat candidate resolution failures as errors rather than evidence that an attached image is gone. Keep a new service provisional over a private inherited socketpair until control-file readiness and public shutdown endpoint checks succeed. On startup failure or caller exit, unmount while FUSE still serves and wait for volume closure and service exit. Report incomplete cleanup explicitly and keep retrying cleanup in the service if unmounting is temporarily blocked. Restore dismounts of released services without /shutdown through a validated legacy flow. Preserve incoming file-protocol notifications and watch for external unmounts independently of those notifications. Legacy unmount cannot guarantee termination of an already-running old service. Use one fixed versioned socket frame and publish the random endpoint in /shutdown-socket, preserving the three-field /shutdown identity. Remove compatibility with unpublished socket protocols. Recover from transient accept and mount-enumeration failures, bound partial-request lifetimes, and report connection refusal as an availability error. Handle join failure without unwinding the destructor or freeing a live worker's context. Map auxiliary EBUSY to MountedVolumeInUse for the GUI force prompt. Return failure for a single busy non-interactive dismount and log automatic-dismount failures. Mark inherited descriptors close-on-exec before FUSE setup and make File::SetCloseOnExec const. Extend disposable-container tests for released clients and services, reused device numbers, partial requests, identity validation, forced write integrity, TMPDIR aliases, and injected startup and rollback failures. Validated without sudo with a clean arm64 build, unchanged warnings, algorithm self-tests, the compatibility matrix, descriptor audits, worker fault-recovery checks, and 24 conditional compilation checks.
This commit is contained in:
14 files changed
+1421
-162
No files matched your search
@@ -53,6 +53,8 @@ namespace VeraCrypt
|
||||
TC_EXCEPTION (LoopDeviceSetupFailed); \
|
||||
TC_EXCEPTION (MountPointRequired); \
|
||||
TC_EXCEPTION (MountPointUnavailable); \
|
||||
TC_EXCEPTION (MountServiceIncompatible); \
|
||||
TC_EXCEPTION (MountServiceCleanupFailed); \
|
||||
TC_EXCEPTION (NoDriveLetterAvailable); \
|
||||
TC_EXCEPTION (TemporaryDirectoryFailure); \
|
||||
TC_EXCEPTION (UnsupportedSectorSizeHiddenVolumeProtection); \
|
||||
|
||||
@@ -602,6 +602,12 @@ namespace VeraCrypt
|
||||
|
||||
mountedVol->AuxMountPoint = mf.MountPoint;
|
||||
|
||||
#ifdef TC_MACOSX
|
||||
// The control file can retain a disk number after detach. Always resolve
|
||||
// the image first, before consulting the mount table for that device.
|
||||
try { UpdateMountedVolumeInfo (mountedVol); }
|
||||
catch (exception &e) { SystemLog::WriteException (e); }
|
||||
#else
|
||||
if (mountedVol->MountPoint.IsEmpty() && !mountedVol->VirtualDevice.IsEmpty())
|
||||
{
|
||||
MountedFilesystemList mpl = GetMountedFilesystems (mountedVol->VirtualDevice);
|
||||
@@ -612,6 +618,7 @@ namespace VeraCrypt
|
||||
|
||||
if (mountedVol->MountPoint.IsEmpty() || mountedVol->VirtualDevice.IsEmpty())
|
||||
UpdateMountedVolumeInfo (mountedVol);
|
||||
#endif
|
||||
|
||||
volumes.push_back (mountedVol);
|
||||
|
||||
@@ -1129,9 +1136,20 @@ namespace VeraCrypt
|
||||
throw DeviceSectorSizeMismatch (SRC_POS, StringConverter::ToWide(devSectorSize) + L" != " + StringConverter::ToWide((uint32) volSectorSize));
|
||||
}
|
||||
|
||||
string fuseMountPoint;
|
||||
#ifdef VC_MACOSX_FUSET
|
||||
// An older service may still be shutting down after its SMB mount has
|
||||
// disappeared. FUSE-T also uses the pathname during backend teardown,
|
||||
// so a replacement volume must have a different auxiliary path.
|
||||
string mountTemplate = string (GetTempDirectory()) + "/" + GetFuseMountDirPrefix() + "-XXXXXXXXXXXX";
|
||||
vector <char> mountDirectory (mountTemplate.begin(), mountTemplate.end());
|
||||
mountDirectory.push_back ('\0');
|
||||
throw_sys_if (mkdtemp (&mountDirectory[0]) == NULL);
|
||||
fuseMountPoint = &mountDirectory[0];
|
||||
throw_sys_if (chmod (fuseMountPoint.c_str(), S_IRUSR | S_IXUSR) == -1);
|
||||
#else
|
||||
// Find a free mount point for FUSE service
|
||||
MountedFilesystemList mountedFilesystems = GetMountedFilesystems ();
|
||||
string fuseMountPoint;
|
||||
for (int i = 1; true; i++)
|
||||
{
|
||||
stringstream path;
|
||||
@@ -1168,6 +1186,7 @@ namespace VeraCrypt
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef VC_MACOSX_FUSET
|
||||
uint64 fuseServiceSerialInstanceNumber;
|
||||
@@ -1175,6 +1194,14 @@ namespace VeraCrypt
|
||||
|
||||
try
|
||||
{
|
||||
#ifdef TC_MACOSX
|
||||
// FUSE canonicalizes its mount path. Give hdiutil the same path so
|
||||
// its inventory identifies the image even when TMPDIR is a symlink.
|
||||
char *canonicalPath = realpath (fuseMountPoint.c_str(), NULL);
|
||||
throw_sys_if (canonicalPath == NULL);
|
||||
finally_do_arg (char *, canonicalPath, { free (finally_arg); });
|
||||
fuseMountPoint = canonicalPath;
|
||||
#endif
|
||||
#ifdef VC_MACOSX_FUSET
|
||||
fuseServiceSerialInstanceNumber = FuseService::Mount (volume, options.SlotNumber, fuseMountPoint);
|
||||
#else
|
||||
|
||||
@@ -96,30 +96,12 @@ namespace VeraCrypt
|
||||
return CFPropertyListCreateWithData (kCFAllocatorDefault, (CFDataRef) data.Get(), kCFPropertyListImmutable, nullptr, nullptr);
|
||||
}
|
||||
|
||||
static string NormalizeDiskImagePath (const string &path)
|
||||
static string CanonicalizeDiskImagePath (const string &path)
|
||||
{
|
||||
string normalized;
|
||||
bool previousSlash = false;
|
||||
|
||||
for (string::const_iterator i = path.begin(); i != path.end(); ++i)
|
||||
{
|
||||
if (*i == '/')
|
||||
{
|
||||
if (previousSlash)
|
||||
continue;
|
||||
|
||||
previousSlash = true;
|
||||
}
|
||||
else
|
||||
previousSlash = false;
|
||||
|
||||
normalized += *i;
|
||||
}
|
||||
|
||||
if (normalized.find ("/private/") == 0)
|
||||
normalized.erase (0, 8);
|
||||
|
||||
return normalized;
|
||||
char *canonicalPath = realpath (path.c_str(), NULL);
|
||||
throw_sys_sub_if (canonicalPath == NULL, path);
|
||||
finally_do_arg (char *, canonicalPath, { free (finally_arg); });
|
||||
return canonicalPath;
|
||||
}
|
||||
|
||||
// Walk a "system-entities" array (from hdiutil attach/info). Prefer the entity
|
||||
@@ -195,15 +177,22 @@ namespace VeraCrypt
|
||||
args.push_back ("-plist");
|
||||
|
||||
string xml = Process::Execute ("/usr/bin/hdiutil", args);
|
||||
string normalizedImagePath = NormalizeDiskImagePath (imagePath);
|
||||
string canonicalImagePath = CanonicalizeDiskImagePath (imagePath);
|
||||
// TMPDIR aliases change the parent path, not VeraCrypt's auxiliary
|
||||
// directory name or the image filename appended to it.
|
||||
size_t imageSeparator = canonicalImagePath.find_last_of ('/');
|
||||
size_t auxiliarySeparator = canonicalImagePath.find_last_of ('/', imageSeparator - 1);
|
||||
if (imageSeparator == string::npos || auxiliarySeparator == string::npos)
|
||||
throw ParameterIncorrect (SRC_POS);
|
||||
const string imageSuffix = canonicalImagePath.substr (auxiliarySeparator);
|
||||
|
||||
CFHolder plist (ParsePropertyList (xml));
|
||||
if (!plist.Get() || CFGetTypeID (plist.Get()) != CFDictionaryGetTypeID())
|
||||
return false;
|
||||
throw ParameterIncorrect (SRC_POS);
|
||||
|
||||
CFTypeRef images = CFDictionaryGetValue ((CFDictionaryRef) plist.Get(), CFSTR ("images")); // borrowed
|
||||
if (!images || CFGetTypeID (images) != CFArrayGetTypeID())
|
||||
return false;
|
||||
throw ParameterIncorrect (SRC_POS);
|
||||
|
||||
CFArrayRef imageArray = (CFArrayRef) images;
|
||||
CFIndex count = CFArrayGetCount (imageArray);
|
||||
@@ -211,21 +200,34 @@ namespace VeraCrypt
|
||||
{
|
||||
CFTypeRef image = CFArrayGetValueAtIndex (imageArray, i); // borrowed
|
||||
if (!image || CFGetTypeID (image) != CFDictionaryGetTypeID())
|
||||
continue;
|
||||
throw ParameterIncorrect (SRC_POS);
|
||||
|
||||
CFDictionaryRef imageDict = (CFDictionaryRef) image;
|
||||
|
||||
string currentImagePath = CFDictionaryGetStdString (imageDict, "image-path");
|
||||
if (NormalizeDiskImagePath (currentImagePath) != normalizedImagePath)
|
||||
continue;
|
||||
if (currentImagePath.empty())
|
||||
throw ParameterIncorrect (SRC_POS);
|
||||
if (currentImagePath != canonicalImagePath)
|
||||
{
|
||||
// Older clients attached through TMPDIR aliases. Resolve those too,
|
||||
// but do not access unrelated disk images (which may be offline).
|
||||
if (currentImagePath[0] != '/' || currentImagePath.size() < imageSuffix.size()
|
||||
|| currentImagePath.compare (currentImagePath.size() - imageSuffix.size(), imageSuffix.size(), imageSuffix) != 0)
|
||||
continue;
|
||||
// Failure to resolve a candidate is not proof that our image is gone.
|
||||
if (CanonicalizeDiskImagePath (currentImagePath) != canonicalImagePath)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Matching image found: extract from its system-entities (mirrors the
|
||||
// previous behavior of returning the result for the first match).
|
||||
// A missing image and an unreadable inventory are different states.
|
||||
// Callers must never fall back to a cached disk number on an error.
|
||||
CFTypeRef entities = CFDictionaryGetValue (imageDict, CFSTR ("system-entities")); // borrowed
|
||||
if (!entities || CFGetTypeID (entities) != CFArrayGetTypeID())
|
||||
return false;
|
||||
throw ParameterIncorrect (SRC_POS);
|
||||
|
||||
return ExtractDeviceAndMountPointFromEntities ((CFArrayRef) entities, device, mountPoint);
|
||||
if (!ExtractDeviceAndMountPointFromEntities ((CFArrayRef) entities, device, mountPoint))
|
||||
throw ParameterIncorrect (SRC_POS);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -268,16 +270,32 @@ namespace VeraCrypt
|
||||
|
||||
shared_ptr <VolumeInfo> CoreMacOSX::DismountVolume (shared_ptr <VolumeInfo> mountedVolume, bool ignoreOpenFiles, bool syncVolumeInfo)
|
||||
{
|
||||
if (!mountedVolume->AuxMountPoint.IsEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateMountedVolumeInfo (mountedVolume);
|
||||
}
|
||||
catch (...) { }
|
||||
}
|
||||
if (!mountedVolume || mountedVolume->AuxMountPoint.IsEmpty())
|
||||
throw ParameterIncorrect (SRC_POS);
|
||||
#ifdef VC_MACOSX_FUSET
|
||||
// Validate the mount and check the service protocol before detaching
|
||||
// the disk image. Retain its identity independently of SMB metadata.
|
||||
const FuseService::DismountRequest dismountRequest = FuseService::PrepareDismount (mountedVolume->AuxMountPoint,
|
||||
mountedVolume->SerialInstanceNumber, mountedVolume->SlotNumber, ignoreOpenFiles);
|
||||
#endif
|
||||
|
||||
if (!mountedVolume->VirtualDevice.IsEmpty() && mountedVolume->VirtualDevice.IsBlockDevice())
|
||||
// Resolve ownership immediately before detach, including retries after a
|
||||
// busy auxiliary unmount or an external eject. BSD disk numbers are reused.
|
||||
#ifdef VC_MACOSX_FUSET
|
||||
if (!FuseService::IsDismountMountPresent (dismountRequest))
|
||||
{
|
||||
if (!dismountRequest.LegacyService)
|
||||
FuseService::WaitForDismount (FuseService::RequestDismount (dismountRequest), mountedVolume->AuxMountPoint, mountedVolume->SlotNumber);
|
||||
return mountedVolume;
|
||||
}
|
||||
#endif
|
||||
UpdateMountedVolumeInfo (mountedVolume);
|
||||
|
||||
if (!mountedVolume->VirtualDevice.IsEmpty() && mountedVolume->VirtualDevice.IsBlockDevice()
|
||||
#ifdef VC_MACOSX_FUSET
|
||||
&& FuseService::IsDismountMountPresent (dismountRequest)
|
||||
#endif
|
||||
)
|
||||
{
|
||||
list <string> args;
|
||||
args.push_back ("detach");
|
||||
@@ -313,29 +331,26 @@ namespace VeraCrypt
|
||||
sync();
|
||||
VolumeInfoList ml = GetMountedVolumes (mountedVolume->Path);
|
||||
|
||||
if (ml.size() > 0)
|
||||
if (ml.size() > 0 && ml.front()->SerialInstanceNumber == mountedVolume->SerialInstanceNumber)
|
||||
mountedVolume = ml.front();
|
||||
}
|
||||
|
||||
#ifdef VC_MACOSX_FUSET
|
||||
pid_t fuseServiceProcessId = FuseService::RequestDismount (mountedVolume->AuxMountPoint,
|
||||
mountedVolume->SerialInstanceNumber, mountedVolume->SlotNumber);
|
||||
#endif
|
||||
|
||||
// The service unmounts SMB while its FUSE loop can still answer requests.
|
||||
if (!dismountRequest.LegacyService)
|
||||
{
|
||||
pid_t fuseServiceProcessId = FuseService::RequestDismount (dismountRequest);
|
||||
FuseService::WaitForDismount (fuseServiceProcessId, mountedVolume->AuxMountPoint, mountedVolume->SlotNumber);
|
||||
}
|
||||
else
|
||||
FuseService::DismountLegacy (dismountRequest);
|
||||
#else
|
||||
list <string> args;
|
||||
args.push_back ("--");
|
||||
args.push_back (mountedVolume->AuxMountPoint);
|
||||
|
||||
for (int t = 0; true; t++)
|
||||
{
|
||||
#ifdef VC_MACOSX_FUSET
|
||||
try
|
||||
{
|
||||
if (GetMountedFilesystems (DevicePath(), mountedVolume->AuxMountPoint).empty())
|
||||
break;
|
||||
}
|
||||
catch (...) { }
|
||||
#endif
|
||||
try
|
||||
{
|
||||
Process::Execute ("/sbin/umount", args);
|
||||
@@ -348,9 +363,6 @@ namespace VeraCrypt
|
||||
Thread::Sleep (200);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef VC_MACOSX_FUSET
|
||||
FuseService::WaitForDismount (fuseServiceProcessId, mountedVolume->AuxMountPoint, mountedVolume->SlotNumber);
|
||||
#endif
|
||||
|
||||
try
|
||||
@@ -364,29 +376,21 @@ namespace VeraCrypt
|
||||
|
||||
void CoreMacOSX::UpdateMountedVolumeInfo (shared_ptr <VolumeInfo> mountedVolume) const
|
||||
{
|
||||
if (!mountedVolume || mountedVolume->AuxMountPoint.IsEmpty())
|
||||
if (!mountedVolume)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
DevicePath recoveredVirtualDevice;
|
||||
DirectoryPath recoveredMountPoint;
|
||||
|
||||
if (FindDiskImageInfoByImagePath (string (mountedVolume->AuxMountPoint) + FuseService::GetVolumeImagePath(), recoveredVirtualDevice, recoveredMountPoint))
|
||||
{
|
||||
if (!recoveredVirtualDevice.IsEmpty())
|
||||
{
|
||||
if (mountedVolume->VirtualDevice != recoveredVirtualDevice && recoveredMountPoint.IsEmpty())
|
||||
mountedVolume->MountPoint = DirectoryPath();
|
||||
|
||||
mountedVolume->VirtualDevice = recoveredVirtualDevice;
|
||||
}
|
||||
|
||||
if (!recoveredMountPoint.IsEmpty())
|
||||
mountedVolume->MountPoint = recoveredMountPoint;
|
||||
}
|
||||
}
|
||||
catch (...) { }
|
||||
// Clear stale metadata even if discovery fails. Destructive callers get
|
||||
// the exception; enumeration can still show the auxiliary mount for retry.
|
||||
mountedVolume->VirtualDevice = DevicePath();
|
||||
mountedVolume->MountPoint = DirectoryPath();
|
||||
if (mountedVolume->AuxMountPoint.IsEmpty())
|
||||
return;
|
||||
DevicePath recoveredVirtualDevice;
|
||||
DirectoryPath recoveredMountPoint;
|
||||
if (!FindDiskImageInfoByImagePath (string (mountedVolume->AuxMountPoint) + FuseService::GetVolumeImagePath(), recoveredVirtualDevice, recoveredMountPoint))
|
||||
return;
|
||||
mountedVolume->VirtualDevice = recoveredVirtualDevice;
|
||||
mountedVolume->MountPoint = recoveredMountPoint;
|
||||
|
||||
if (mountedVolume->MountPoint.IsEmpty() && !mountedVolume->VirtualDevice.IsEmpty())
|
||||
{
|
||||
@@ -434,6 +438,7 @@ namespace VeraCrypt
|
||||
|
||||
void CoreMacOSX::CheckFilesystem (shared_ptr <VolumeInfo> mountedVolume, bool repair) const
|
||||
{
|
||||
UpdateMountedVolumeInfo (mountedVolume);
|
||||
// Honor the check-vs-repair distinction by running diskutil on the VeraCrypt
|
||||
// virtual device (diskutil unmounts the inner filesystem itself as needed).
|
||||
// The Core layer has no GUI, so results are shown in a Terminal window via a
|
||||
|
||||
Reference in new issue
Block a user