From d8fcbcc87b1835bd71840fe29e648f5852f18682 Mon Sep 17 00:00:00 2001 From: Ville Takio Date: Tue, 27 Oct 2026 05:12:12 +0200 Subject: [PATCH 1/4] Linux: do not mount a volume again while a detached service keeps it open An emergency dismount lazily detaches the file systems. The FUSE service keeps running with the volume open until the last file on the detached file system is closed, but the volume is no longer in the mount table, so it could be mounted again and two services wrote to it. Once the volume is open, take an exclusive flock for it. The FUSE service inherits the descriptor and holds the lock until it exits; a second mount fails with "already mounted" meanwhile. Because a normally dismounted service may still be exiting, a busy lock is retried for up to 10 seconds. The lock is named after the device number, or the file system and inode numbers, that fstat reports for the opened volume, so it applies to the file actually served, whatever path leads to it. It is not taken on the volume file itself, whose locking is disabled to avoid remote file system issues. The lock files are created with openat in the root owned /var/run/veracrypt, which is opened without following symbolic links and checked through its descriptor. /var/run also exists on systems without /run. Without root or a usable lock directory, mounting works as before. Fixes #1959 --- src/Core/Unix/CoreUnix.cpp | 82 ++++++++++++++++++++++++++++++++++++++ src/Platform/File.h | 1 + 2 files changed, 83 insertions(+) diff --git a/src/Core/Unix/CoreUnix.cpp b/src/Core/Unix/CoreUnix.cpp index 6055c4c8e6..627b797743 100644 --- a/src/Core/Unix/CoreUnix.cpp +++ b/src/Core/Unix/CoreUnix.cpp @@ -25,6 +25,8 @@ #include #endif #ifdef TC_LINUX +#include +#include #include #endif #include @@ -1245,6 +1247,76 @@ namespace VeraCrypt throw MountPointNotAllowed (SRC_POS); } +#ifdef TC_LINUX + // Opens the lock file 'name' in the root owned /var/run/veracrypt, or returns + // -1 if that is not possible (not root, or no usable directory). The locks + // are taken with flock, so they last until the last copy of the descriptor is + // closed: the copy that the FUSE service inherits lasts until it exits. + static int OpenMountLockFile (const string &name) + { + if (geteuid() != 0) + return -1; + + // /var/run also exists on systems without /run + const char *lockDirectory = "/var/run/veracrypt"; + if (mkdir (lockDirectory, S_IRWXU) == -1 && errno != EEXIST) + return -1; + + // Keep using the directory that was checked + int directoryFd = open (lockDirectory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (directoryFd == -1) + return -1; + finally_do_arg (int, directoryFd, { close (finally_arg); }); + + struct stat directoryInfo; + if (fstat (directoryFd, &directoryInfo) == -1 || directoryInfo.st_uid != 0 + || (directoryInfo.st_mode & (S_IWGRP | S_IWOTH))) + return -1; + + return openat (directoryFd, name.c_str(), O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC, S_IRUSR | S_IWUSR); + } + + // Returns a descriptor holding an exclusive lock for the open volume file, or + // -1 if no lock can be created. + static int LockVolumeForMount (const File &volumeFile) + { + struct stat volumeInfo; + if (fstat (volumeFile.GetSystemHandle(), &volumeInfo) == -1) + return -1; + + stringstream lockName; + lockName << hex; + if (S_ISBLK (volumeInfo.st_mode)) + lockName << "device-" << (uint64) volumeInfo.st_rdev; + else + lockName << "file-" << (uint64) volumeInfo.st_dev << "-" << (uint64) volumeInfo.st_ino; + + int fd = OpenMountLockFile (lockName.str()); + if (fd == -1) + return -1; + + // A service that has just been dismounted normally may still be exiting + for (int t = 0; flock (fd, LOCK_EX | LOCK_NB) == -1; t++) + { + const int error = errno; + if (error == EINTR) + continue; + + if (error != EWOULDBLOCK || t >= 100) + { + close (fd); + if (error == EWOULDBLOCK) + throw VolumeAlreadyMounted (SRC_POS); + return -1; + } + + Thread::Sleep (100); + } + + return fd; + } +#endif + shared_ptr CoreUnix::MountVolume (MountOptions &options) { CoalesceSlotNumberAndMountPoint (options); @@ -1301,6 +1373,16 @@ namespace VeraCrypt break; } +#ifdef TC_LINUX + // An emergency dismount only detaches the file systems: the FUSE service + // keeps the volume open after it has left the mount table, until the + // last file on the volume is closed. Mounting the volume again meanwhile + // would let two services write to it. The lock identifies the file that + // was opened, whatever path leads to it. + int volumeLock = LockVolumeForMount (*volume->GetFile()); + finally_do_arg (int, volumeLock, { if (finally_arg != -1) close (finally_arg); }); +#endif + // Opening a partially encrypted system volume can force read-only // protection. Carry that effective mode into either mount backend. if (volume->GetProtectionType() == VolumeProtection::ReadOnly) diff --git a/src/Platform/File.h b/src/Platform/File.h index 9c870c739c..99a9e02f21 100644 --- a/src/Platform/File.h +++ b/src/Platform/File.h @@ -71,6 +71,7 @@ namespace VeraCrypt { } virtual ~File (); + SystemFileHandleType GetSystemHandle () const { return FileHandle; } void AssignSystemHandle (SystemFileHandleType openFileHandle, bool sharedHandle = true) { if (FileIsOpen) From f0d16ec6e31ecf5a258e7db690cf12acf42cffb1 Mon Sep 17 00:00:00 2001 From: Ville Takio Date: Tue, 27 Oct 2026 22:56:57 +0200 Subject: [PATCH 2/4] Linux: refuse a volume that a kernel device or another mount still uses With kernel crypto, the volume is used by a device-mapper device, which an emergency dismount removes deferred. The FUSE service exits, so the volume lock was released while the device still used the volume, and the volume could be mounted again in another slot. Before taking the lock, refuse a volume device held by a device-mapper device, or a volume file backing a loop device. A volume mounted under another path, for example a device alias, made a second mount wait for the lock for 10 seconds. Refuse at once when a mounted volume is the same file or device. --auto-mount=devices skips such a device instead of stopping at it. --- src/Core/Unix/CoreUnix.cpp | 33 ++++++++++++++++++-- src/Core/Unix/CoreUnix.h | 2 ++ src/Core/Unix/Linux/CoreLinux.cpp | 50 +++++++++++++++++++++++++++++++ src/Core/Unix/Linux/CoreLinux.h | 1 + src/Main/UserInterface.cpp | 2 ++ 5 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/Core/Unix/CoreUnix.cpp b/src/Core/Unix/CoreUnix.cpp index 627b797743..800e531b98 100644 --- a/src/Core/Unix/CoreUnix.cpp +++ b/src/Core/Unix/CoreUnix.cpp @@ -1276,14 +1276,27 @@ namespace VeraCrypt return openat (directoryFd, name.c_str(), O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC, S_IRUSR | S_IWUSR); } + static bool IsSameFile (const struct stat &a, const struct stat &b) + { + if (S_ISBLK (a.st_mode) || S_ISBLK (b.st_mode)) + return S_ISBLK (a.st_mode) && S_ISBLK (b.st_mode) && a.st_rdev == b.st_rdev; + + return a.st_dev == b.st_dev && a.st_ino == b.st_ino; + } + // Returns a descriptor holding an exclusive lock for the open volume file, or // -1 if no lock can be created. - static int LockVolumeForMount (const File &volumeFile) + static int LockVolumeForMount (const CoreUnix &core, const File &volumeFile) { struct stat volumeInfo; if (fstat (volumeFile.GetSystemHandle(), &volumeInfo) == -1) return -1; + // With kernel crypto, a device-mapper device can keep using the volume + // after the FUSE service of an emergency dismount has exited + if (core.IsVolumeUsedByKernelDevice (volumeInfo)) + throw VolumeAlreadyMounted (SRC_POS); + stringstream lockName; lockName << hex; if (S_ISBLK (volumeInfo.st_mode)) @@ -1295,7 +1308,6 @@ namespace VeraCrypt if (fd == -1) return -1; - // A service that has just been dismounted normally may still be exiting for (int t = 0; flock (fd, LOCK_EX | LOCK_NB) == -1; t++) { const int error = errno; @@ -1310,6 +1322,21 @@ namespace VeraCrypt return -1; } + // Mounted under another path: refuse without waiting + if (t == 0) + { + foreach_ref (const VolumeInfo &mounted, core.GetMountedVolumes()) + { + struct stat mountedInfo; + if (stat (string (mounted.Path).c_str(), &mountedInfo) == 0 && IsSameFile (mountedInfo, volumeInfo)) + { + close (fd); + throw VolumeAlreadyMounted (SRC_POS); + } + } + } + + // A service that has just been dismounted normally may still be exiting Thread::Sleep (100); } @@ -1379,7 +1406,7 @@ namespace VeraCrypt // last file on the volume is closed. Mounting the volume again meanwhile // would let two services write to it. The lock identifies the file that // was opened, whatever path leads to it. - int volumeLock = LockVolumeForMount (*volume->GetFile()); + int volumeLock = LockVolumeForMount (*this, *volume->GetFile()); finally_do_arg (int, volumeLock, { if (finally_arg != -1) close (finally_arg); }); #endif diff --git a/src/Core/Unix/CoreUnix.h b/src/Core/Unix/CoreUnix.h index 968a7c2d4d..5ff326304d 100644 --- a/src/Core/Unix/CoreUnix.h +++ b/src/Core/Unix/CoreUnix.h @@ -13,6 +13,7 @@ #ifndef TC_HEADER_Core_CoreUnix #define TC_HEADER_Core_CoreUnix +#include #include "System.h" #include "Platform/Unix/Process.h" #include "Core/CoreBase.h" @@ -43,6 +44,7 @@ namespace VeraCrypt virtual bool IsDevicePresent (const DevicePath &device) const { throw NotApplicable (SRC_POS); } virtual bool IsInPortableMode () const { return false; } virtual bool IsMountPointAvailable (const DirectoryPath &mountPoint) const; + virtual bool IsVolumeUsedByKernelDevice (const struct stat &volumeStat) const { return false; } virtual bool IsOSVersion (int major, int minor) const { throw NotApplicable (SRC_POS); } virtual bool IsOSVersionLower (int major, int minor) const { throw NotApplicable (SRC_POS); } virtual bool IsPasswordCacheEmpty () const { throw NotApplicable (SRC_POS); } diff --git a/src/Core/Unix/Linux/CoreLinux.cpp b/src/Core/Unix/Linux/CoreLinux.cpp index 9f4651a006..f757330a28 100644 --- a/src/Core/Unix/Linux/CoreLinux.cpp +++ b/src/Core/Unix/Linux/CoreLinux.cpp @@ -718,6 +718,56 @@ namespace VeraCrypt DismountNativeVolumeInternal (mountedVolume, false); } + bool CoreLinux::IsVolumeUsedByKernelDevice (const struct stat &volumeStat) const + { + if (S_ISBLK (volumeStat.st_mode)) + { + // A device-mapper device on the volume device + stringstream holders; + holders << "/sys/dev/block/" << major (volumeStat.st_rdev) << ":" << minor (volumeStat.st_rdev) << "/holders"; + + DIR *dir = opendir (holders.str().c_str()); + if (!dir) + return false; + finally_do_arg (DIR *, dir, { closedir (finally_arg); }); + + struct dirent *entry; + while ((entry = readdir (dir)) != nullptr) + { + if (string (entry->d_name).find ("dm-") == 0) + return true; + } + + return false; + } + + if (!S_ISREG (volumeStat.st_mode)) + return false; + + // A loop device of the volume file, which carries the device-mapper device + DIR *sysBlock = opendir ("/sys/block"); + if (!sysBlock) + return false; + finally_do_arg (DIR *, sysBlock, { closedir (finally_arg); }); + + struct dirent *entry; + while ((entry = readdir (sysBlock)) != nullptr) + { + const string kernelName = entry->d_name; + if (kernelName.find ("loop") != 0) + continue; + + try + { + if (IsLoopDeviceBackedBy ("/dev/" + kernelName, LoopBacking (volumeStat))) + return true; + } + catch (...) { } + } + + return false; + } + void CoreLinux::DismountNativeVolumeDeferred (shared_ptr mountedVolume) const { DismountNativeVolumeInternal (mountedVolume, true); diff --git a/src/Core/Unix/Linux/CoreLinux.h b/src/Core/Unix/Linux/CoreLinux.h index 43da5133f0..8281b1212e 100644 --- a/src/Core/Unix/Linux/CoreLinux.h +++ b/src/Core/Unix/Linux/CoreLinux.h @@ -28,6 +28,7 @@ namespace VeraCrypt virtual shared_ptr DismountVolume (shared_ptr mountedVolume, bool ignoreOpenFiles = false, bool syncVolumeInfo = false); virtual shared_ptr EmergencyDismountVolume (shared_ptr mountedVolume); virtual HostDeviceList GetHostDevices (bool pathListOnly = false) const; + virtual bool IsVolumeUsedByKernelDevice (const struct stat &volumeStat) const; protected: virtual DevicePath AttachFileToLoopDevice (const FilePath &filePath, bool readOnly) const; diff --git a/src/Main/UserInterface.cpp b/src/Main/UserInterface.cpp index da14623e8a..4385b2ebcd 100644 --- a/src/Main/UserInterface.cpp +++ b/src/Main/UserInterface.cpp @@ -841,6 +841,8 @@ namespace VeraCrypt catch (PasswordException&) { } catch (SystemException&) { } catch (ExecutedProcessFailed&) { } + // Already mounted under another path + catch (VolumeAlreadyMounted&) { } } if (newMountedVolumes.empty()) From e9640b82e0a0ca90e414f5737d0b4879e4e9e18a Mon Sep 17 00:00:00 2001 From: Ville Takio Date: Wed, 28 Oct 2026 22:58:31 +0200 Subject: [PATCH 3/4] Linux: refuse a writable mount whose volume use cannot be established The volume lock and the kernel device check let mounting continue when they could not establish that the volume was unused: an unusable lock directory, an unexpected flock error, a sysfs or loop device that could not be inspected, or an fstat failing on a file opened with the requester's credentials, as FUSE checks the credentials of the calling process. A writable mount is now refused in these cases; a read-only one continues as before. A loop device that disappears during the check is still taken as not using the volume. The requester's process passes the device and inode of the file it opened, and the lock uses them for such files instead of fstat. The kernel device check is repeated once the lock is held: another mount of the volume may have finished, and been dismounted in an emergency, while this one was waiting for it. The lock descriptor is closed if an exception occurs before it is returned. The lock directory must belong to the user running VeraCrypt, root, and be writable only by it. CoreUnix::GetMountLockDirectory() gives its location, which the unprivileged mount test overrides, so that it uses the production locks instead of failing to create /var/run/veracrypt. --- Tests/linux_mount_points_test.cpp | 2 + Tests/linux_mount_read_only_test.cpp | 2 + src/Core/Unix/CoreUnix.cpp | 99 +++++++++++++++++---------- src/Core/Unix/CoreUnix.h | 2 + src/Core/Unix/Linux/CoreLinux.cpp | 22 +++--- src/Platform/File.h | 13 +++- src/Platform/Unix/File.cpp | 17 +++++ src/Platform/Unix/RequesterAccess.cpp | 6 ++ 8 files changed, 110 insertions(+), 53 deletions(-) diff --git a/Tests/linux_mount_points_test.cpp b/Tests/linux_mount_points_test.cpp index 3073a618a8..633658e65a 100644 --- a/Tests/linux_mount_points_test.cpp +++ b/Tests/linux_mount_points_test.cpp @@ -77,6 +77,8 @@ struct TestCore : CoreLinux MountedFilesystemList GetMountedFilesystems (const DevicePath & = DevicePath(), const DirectoryPath & = DirectoryPath()) const override { return MountedFilesystemList(); } string GetTempDirectory () const override { return Root; } + // The production locks, in a directory that the unprivileged test can create + string GetMountLockDirectory () const override { return Root + "/locks"; } shared_ptr GetMountedVolume (const VolumePath &) const override { shared_ptr info (new VolumeInfo); diff --git a/Tests/linux_mount_read_only_test.cpp b/Tests/linux_mount_read_only_test.cpp index 9fc3f2892f..7f0ed2b378 100644 --- a/Tests/linux_mount_read_only_test.cpp +++ b/Tests/linux_mount_read_only_test.cpp @@ -133,6 +133,8 @@ struct FallbackCore : TestCore bool, VolumeType::Enum, bool, bool) const override { return Opened; } bool IsVolumeMounted (const VolumePath &) const override { return false; } VolumeInfoList GetMountedVolumes (const VolumePath & = VolumePath()) const override { return VolumeInfoList(); } + // The production locks, in a directory that the unprivileged test can create + string GetMountLockDirectory () const override { return GetTempDirectory() + "/locks"; } shared_ptr GetMountedVolume (const VolumePath &) const override { return shared_ptr (new VolumeInfo); diff --git a/src/Core/Unix/CoreUnix.cpp b/src/Core/Unix/CoreUnix.cpp index 800e531b98..92cfb8ff6c 100644 --- a/src/Core/Unix/CoreUnix.cpp +++ b/src/Core/Unix/CoreUnix.cpp @@ -1248,32 +1248,31 @@ namespace VeraCrypt } #ifdef TC_LINUX - // Opens the lock file 'name' in the root owned /var/run/veracrypt, or returns - // -1 if that is not possible (not root, or no usable directory). The locks - // are taken with flock, so they last until the last copy of the descriptor is - // closed: the copy that the FUSE service inherits lasts until it exits. - static int OpenMountLockFile (const string &name) + // Opens the lock file 'name' in the lock directory, which must belong to the + // effective user (root, /var/run/veracrypt) and be writable only by it. The + // locks are taken with flock, so they last until the last copy of the + // descriptor is closed: the copy that the FUSE service inherits lasts until it + // exits. + static int OpenMountLockFile (const string &lockDirectory, const string &name) { - if (geteuid() != 0) - return -1; - - // /var/run also exists on systems without /run - const char *lockDirectory = "/var/run/veracrypt"; - if (mkdir (lockDirectory, S_IRWXU) == -1 && errno != EEXIST) - return -1; + throw_sys_sub_if (mkdir (lockDirectory.c_str(), S_IRWXU) == -1 && errno != EEXIST, lockDirectory); // Keep using the directory that was checked - int directoryFd = open (lockDirectory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); - if (directoryFd == -1) - return -1; + int directoryFd = open (lockDirectory.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + throw_sys_sub_if (directoryFd == -1, lockDirectory); finally_do_arg (int, directoryFd, { close (finally_arg); }); struct stat directoryInfo; - if (fstat (directoryFd, &directoryInfo) == -1 || directoryInfo.st_uid != 0 - || (directoryInfo.st_mode & (S_IWGRP | S_IWOTH))) - return -1; + throw_sys_sub_if (fstat (directoryFd, &directoryInfo) == -1, lockDirectory); + if (directoryInfo.st_uid != geteuid() || (directoryInfo.st_mode & (S_IWGRP | S_IWOTH))) + { + errno = EPERM; + throw SystemException (SRC_POS, lockDirectory + ": the directory must belong to the user running VeraCrypt and be writable only by it."); + } - return openat (directoryFd, name.c_str(), O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC, S_IRUSR | S_IWUSR); + int fd = openat (directoryFd, name.c_str(), O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC, S_IRUSR | S_IWUSR); + throw_sys_sub_if (fd == -1, lockDirectory + "/" + name); + return fd; } static bool IsSameFile (const struct stat &a, const struct stat &b) @@ -1284,14 +1283,11 @@ namespace VeraCrypt return a.st_dev == b.st_dev && a.st_ino == b.st_ino; } - // Returns a descriptor holding an exclusive lock for the open volume file, or - // -1 if no lock can be created. - static int LockVolumeForMount (const CoreUnix &core, const File &volumeFile) + // Throws VolumeAlreadyMounted if the volume is in use, or another exception if + // that cannot be established. Returns a descriptor holding an exclusive lock + // for the volume, which the FUSE service inherits. + static int LockVolumeIdentity (const CoreUnix &core, const struct stat &volumeInfo) { - struct stat volumeInfo; - if (fstat (volumeFile.GetSystemHandle(), &volumeInfo) == -1) - return -1; - // With kernel crypto, a device-mapper device can keep using the volume // after the FUSE service of an emergency dismount has exited if (core.IsVolumeUsedByKernelDevice (volumeInfo)) @@ -1304,9 +1300,9 @@ namespace VeraCrypt else lockName << "file-" << (uint64) volumeInfo.st_dev << "-" << (uint64) volumeInfo.st_ino; - int fd = OpenMountLockFile (lockName.str()); - if (fd == -1) - return -1; + int fd = OpenMountLockFile (core.GetMountLockDirectory(), lockName.str()); + bool locked = false; + finally_do_arg2 (int, fd, bool &, locked, { if (!finally_arg2) close (finally_arg); }); for (int t = 0; flock (fd, LOCK_EX | LOCK_NB) == -1; t++) { @@ -1314,14 +1310,15 @@ namespace VeraCrypt if (error == EINTR) continue; - if (error != EWOULDBLOCK || t >= 100) + if (error != EWOULDBLOCK) { - close (fd); - if (error == EWOULDBLOCK) - throw VolumeAlreadyMounted (SRC_POS); - return -1; + errno = error; + throw SystemException (SRC_POS, lockName.str()); } + if (t >= 100) + throw VolumeAlreadyMounted (SRC_POS); + // Mounted under another path: refuse without waiting if (t == 0) { @@ -1329,10 +1326,7 @@ namespace VeraCrypt { struct stat mountedInfo; if (stat (string (mounted.Path).c_str(), &mountedInfo) == 0 && IsSameFile (mountedInfo, volumeInfo)) - { - close (fd); throw VolumeAlreadyMounted (SRC_POS); - } } } @@ -1340,8 +1334,37 @@ namespace VeraCrypt Thread::Sleep (100); } + // Another mount may have finished, and been dismounted in an emergency, + // while this one was waiting + if (core.IsVolumeUsedByKernelDevice (volumeInfo)) + throw VolumeAlreadyMounted (SRC_POS); + + locked = true; return fd; } + + // Returns -1 only for a read-only mount whose volume cannot be locked: a + // writable mount is refused unless the volume is known not to be in use. + static int LockVolumeForMount (const CoreUnix &core, const File &volumeFile, bool writable) + { + try + { + struct stat volumeInfo; + volumeFile.GetStat (volumeInfo); + return LockVolumeIdentity (core, volumeInfo); + } + catch (VolumeAlreadyMounted&) + { + throw; + } + catch (...) + { + if (writable) + throw; + } + + return -1; + } #endif shared_ptr CoreUnix::MountVolume (MountOptions &options) @@ -1406,7 +1429,7 @@ namespace VeraCrypt // last file on the volume is closed. Mounting the volume again meanwhile // would let two services write to it. The lock identifies the file that // was opened, whatever path leads to it. - int volumeLock = LockVolumeForMount (*this, *volume->GetFile()); + int volumeLock = LockVolumeForMount (*this, *volume->GetFile(), volume->GetProtectionType() != VolumeProtection::ReadOnly); finally_do_arg (int, volumeLock, { if (finally_arg != -1) close (finally_arg); }); #endif diff --git a/src/Core/Unix/CoreUnix.h b/src/Core/Unix/CoreUnix.h index 5ff326304d..5bc0e9f822 100644 --- a/src/Core/Unix/CoreUnix.h +++ b/src/Core/Unix/CoreUnix.h @@ -45,6 +45,8 @@ namespace VeraCrypt virtual bool IsInPortableMode () const { return false; } virtual bool IsMountPointAvailable (const DirectoryPath &mountPoint) const; virtual bool IsVolumeUsedByKernelDevice (const struct stat &volumeStat) const { return false; } + // Directory of the locks that reserve volumes and slots while mounted + virtual string GetMountLockDirectory () const { return "/var/run/veracrypt"; } virtual bool IsOSVersion (int major, int minor) const { throw NotApplicable (SRC_POS); } virtual bool IsOSVersionLower (int major, int minor) const { throw NotApplicable (SRC_POS); } virtual bool IsPasswordCacheEmpty () const { throw NotApplicable (SRC_POS); } diff --git a/src/Core/Unix/Linux/CoreLinux.cpp b/src/Core/Unix/Linux/CoreLinux.cpp index f757330a28..4a43620bf8 100644 --- a/src/Core/Unix/Linux/CoreLinux.cpp +++ b/src/Core/Unix/Linux/CoreLinux.cpp @@ -718,6 +718,8 @@ namespace VeraCrypt DismountNativeVolumeInternal (mountedVolume, false); } + // Throws if the devices cannot be inspected, which must not be taken for the + // absence of a device-mapper device using the volume. bool CoreLinux::IsVolumeUsedByKernelDevice (const struct stat &volumeStat) const { if (S_ISBLK (volumeStat.st_mode)) @@ -727,8 +729,7 @@ namespace VeraCrypt holders << "/sys/dev/block/" << major (volumeStat.st_rdev) << ":" << minor (volumeStat.st_rdev) << "/holders"; DIR *dir = opendir (holders.str().c_str()); - if (!dir) - return false; + throw_sys_sub_if (!dir, holders.str()); finally_do_arg (DIR *, dir, { closedir (finally_arg); }); struct dirent *entry; @@ -744,25 +745,18 @@ namespace VeraCrypt if (!S_ISREG (volumeStat.st_mode)) return false; - // A loop device of the volume file, which carries the device-mapper device + // A loop device of the volume file, which carries the device-mapper device. + // A loop device that disappears meanwhile is not using it. DIR *sysBlock = opendir ("/sys/block"); - if (!sysBlock) - return false; + throw_sys_sub_if (!sysBlock, "/sys/block"); finally_do_arg (DIR *, sysBlock, { closedir (finally_arg); }); struct dirent *entry; while ((entry = readdir (sysBlock)) != nullptr) { const string kernelName = entry->d_name; - if (kernelName.find ("loop") != 0) - continue; - - try - { - if (IsLoopDeviceBackedBy ("/dev/" + kernelName, LoopBacking (volumeStat))) - return true; - } - catch (...) { } + if (kernelName.find ("loop") == 0 && IsLoopDeviceBackedBy ("/dev/" + kernelName, LoopBacking (volumeStat))) + return true; } return false; diff --git a/src/Platform/File.h b/src/Platform/File.h index 99a9e02f21..c6514c6dfd 100644 --- a/src/Platform/File.h +++ b/src/Platform/File.h @@ -17,6 +17,9 @@ #include "Buffer.h" #include "FilesystemPath.h" #include "SystemException.h" +#ifndef TC_WINDOWS +#include +#endif #ifdef TC_LINUX #define TC_REQUESTER_HELPER_CMDLINE_OPTION "--requester-helper" @@ -66,7 +69,7 @@ namespace VeraCrypt ,AccTime(0), ModTime (0) #endif #ifdef TC_LINUX - ,RequesterBacked (false), ReleaseOnly (false), RequesterUserId (0), RequesterGroupId (0) + ,RequesterBacked (false), ReleaseOnly (false), RequesterUserId (0), RequesterGroupId (0), RequesterDevice (0), RequesterInode (0) #endif { } virtual ~File (); @@ -100,6 +103,12 @@ namespace VeraCrypt void SetReleaseOnly () { ReleaseOnly = true; } #endif FilePath GetPath () const; +#ifndef TC_WINDOWS + // For a file opened with the requester's credentials, the device and inode + // as the requester's process found them: fstat by the caller can fail, as + // FUSE checks the credentials of the calling process. + void GetStat (struct stat &statData) const; +#endif uint64 Length () const; void Open (const FilePath &path, FileOpenMode mode = OpenRead, FileShareMode shareMode = ShareReadWrite, FileOpenFlags flags = FlagsNone); uint64 Read (const BufferPtr &buffer) const; @@ -140,6 +149,8 @@ namespace VeraCrypt bool ReleaseOnly; uid_t RequesterUserId; gid_t RequesterGroupId; + uint64 RequesterDevice; + uint64 RequesterInode; #endif private: diff --git a/src/Platform/Unix/File.cpp b/src/Platform/Unix/File.cpp index ef6cb2738c..8a04f44c0a 100644 --- a/src/Platform/Unix/File.cpp +++ b/src/Platform/Unix/File.cpp @@ -219,6 +219,23 @@ namespace VeraCrypt #endif } + void File::GetStat (struct stat &statData) const + { + if_debug (ValidateState()); + +#ifdef TC_LINUX + if (RequesterBacked) + { + Memory::Zero (&statData, sizeof (statData)); + statData.st_mode = S_IFREG; + statData.st_dev = (dev_t) RequesterDevice; + statData.st_ino = (ino_t) RequesterInode; + return; + } +#endif + throw_sys_sub_if (fstat (FileHandle, &statData) == -1, wstring (Path)); + } + uint64 File::Length () const { if_debug (ValidateState()); diff --git a/src/Platform/Unix/RequesterAccess.cpp b/src/Platform/Unix/RequesterAccess.cpp index ecb3e533f5..e43b027ac5 100644 --- a/src/Platform/Unix/RequesterAccess.cpp +++ b/src/Platform/Unix/RequesterAccess.cpp @@ -59,6 +59,8 @@ namespace VeraCrypt int32 Error; int64 AccessTime; int64 ModificationTime; + uint64 Device; + uint64 Inode; }; // Both ends run the same executable (/proc/self/exe), including after an upgrade. @@ -364,6 +366,8 @@ namespace VeraCrypt reply.Stage = RequesterHelperSucceeded; reply.AccessTime = statData.st_atime; reply.ModificationTime = statData.st_mtime; + reply.Device = statData.st_dev; + reply.Inode = statData.st_ino; } } @@ -548,6 +552,8 @@ namespace VeraCrypt FileHandle = descriptor; AccTime = static_cast (reply.AccessTime); ModTime = static_cast (reply.ModificationTime); + RequesterDevice = reply.Device; + RequesterInode = reply.Inode; RequesterBacked = true; ReleaseOnly = false; RequesterUserId = userId; From a579445ca05eda637d153444e828112e9466a193 Mon Sep 17 00:00:00 2001 From: Ville Takio Date: Tue, 27 Oct 2026 13:40:42 +0200 Subject: [PATCH 4/4] Tests: add a regression test for #1959 Tests/test_linux_emergency_dismount_remount.py checks no second mount while an emergency-detached service keeps the volume open against the installed package in the Linux workflow. --- .github/workflows/build-linux.yml | 3 + .../test_linux_emergency_dismount_remount.py | 225 ++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 Tests/test_linux_emergency_dismount_remount.py diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 6379b82ce8..83da8c2f5b 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -28,6 +28,7 @@ on: - 'Tests/test_stdin_password.py' - 'Tests/test_mount_helper.py' - 'Tests/test_linux_mount_point_protection.py' + - 'Tests/test_linux_emergency_dismount_remount.py' - 'Tests/test.*.hc' - 'Tests/test_volume_read.py' - 'Tests/volume_read_test.cpp' @@ -64,6 +65,7 @@ on: - 'Tests/test_stdin_password.py' - 'Tests/test_mount_helper.py' - 'Tests/test_linux_mount_point_protection.py' + - 'Tests/test_linux_emergency_dismount_remount.py' - 'Tests/test.*.hc' - 'Tests/test_volume_read.py' - 'Tests/volume_read_test.cpp' @@ -221,6 +223,7 @@ jobs: sudo python3 Tests/test_stdin_password.py --require-locales sudo python3 Tests/test_mount_helper.py --helper /usr/sbin/mount.veracrypt sudo python3 Tests/test_linux_mount_point_protection.py + sudo python3 Tests/test_linux_emergency_dismount_remount.py sudo apt remove -y veracrypt - name: Install and test VeraCrypt Console .deb packages diff --git a/Tests/test_linux_emergency_dismount_remount.py b/Tests/test_linux_emergency_dismount_remount.py new file mode 100644 index 0000000000..a0af32ffdb --- /dev/null +++ b/Tests/test_linux_emergency_dismount_remount.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +"""Regression test for issue #1959, against the installed veracrypt. + +Usage (as root, after installing a package): + python3 Tests/test_linux_emergency_dismount_remount.py [--veracrypt PATH] + +Mounts a small volume and keeps a file on it open. After an emergency +dismount the volume stays in use, by the FUSE service (nokernelcrypto) or by +the deferred device-mapper device (kernel crypto), so mounting the volume +again must fail; it must work once the file is closed and the volume has +been released. Also checks that mounting a volume through another path +while it is mounted fails at once. +""" + +import argparse +import os +import shutil +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +PASSWORD_A = "regression-test-password-a" +# A low PIM keeps the tests fast; it needs passwords of 20 characters or more. +COMMON = ["--pim=1", "--keyfiles="] + + +class Checks: + def __init__(self): + self.failures = 0 + + def check(self, desc, cond, detail=""): + print(("ok " if cond else "FAIL ") + desc, flush=True) + if not cond: + self.failures += 1 + if detail: + print(" " + detail.strip().replace("\n", "\n ")) + + +def text(result): + return (result.stdout or b"").decode("utf-8", "replace") + + +class VeraCrypt: + def __init__(self, path, work): + self.path = path + self.binary = os.path.realpath(path) + self.work = work + + def run(self, args, stdin=None, timeout=300): + return subprocess.run([self.path, "--text"] + args, input=stdin, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, cwd=self.work, + timeout=timeout) + + def create(self, path, size, password, filesystem, volume_type="normal"): + return self.run(["--non-interactive", "--create", str(path), + "--size=" + size, "--volume-type=" + volume_type, + "--encryption=AES", "--hash=SHA-512", + "--filesystem=" + filesystem, "-p", password, + "--random-source=" + str(self.work / "random")] + + COMMON) + + def dismount(self, target): + return self.run(["--non-interactive", "-d", str(target)]) + + def mounted_volumes(self): + """Paths of the mounted volumes; raises if they cannot be listed.""" + r = self.run(["--list"]) + if r.returncode != 0: + if "No volumes mounted" in text(r): + return set() + raise RuntimeError("veracrypt --list failed: " + text(r)) + return {line.split()[1] for line in text(r).splitlines() + if len(line.split()) >= 4 and line.split()[0].endswith(":")} + + def dismount_volumes(self, volumes): + """Dismounts only these volumes; returns whether none remains.""" + paths = {str(volume) for volume in volumes} + for path in paths & self.mounted_volumes(): + self.dismount(path) + return not paths & self.mounted_volumes() + + def service(self, volume): + """PID of the FUSE service that has the volume file open.""" + volume = os.path.realpath(volume) + for entry in os.listdir("/proc"): + if not entry.isdigit(): + continue + try: + if os.readlink("/proc/%s/exe" % entry) != self.binary: + continue + fds = ["/proc/%s/fd/%s" % (entry, fd) + for fd in os.listdir("/proc/%s/fd" % entry)] + targets = {os.readlink(fd) for fd in fds} + except OSError: + continue + if "/dev/fuse" in targets and volume in targets: + return int(entry) + return None + + +def wait_for_exit(pid, timeout): + end = time.monotonic() + timeout + while time.monotonic() < end: + if not os.path.exists("/proc/%d" % pid): + return True + time.sleep(0.5) + return False + + +def mount_options(kernel_crypto): + return COMMON + ["--protect-hidden=no"] + ( + [] if kernel_crypto else ["--mount-options=nokernelcrypto"]) + + +def mount(vc, volume, mount_point, kernel_crypto=False): + return vc.run(["--non-interactive", "-p", PASSWORD_A] + + mount_options(kernel_crypto) + + [str(volume), str(mount_point)]) + + +def listed_device(vc, volume): + for line in text(vc.run(["--list"])).splitlines(): + fields = line.split() + if len(fields) >= 4 and fields[1] == str(volume): + return fields[2] + return "" + + +def test_detached_service(vc, c, volume, mount_point, second_mount_point, + kernel_crypto): + name = "#1959 (%s)" % ("kernel crypto" if kernel_crypto else "FUSE") + r = mount(vc, volume, mount_point, kernel_crypto) + c.check("%s: mount" % name, r.returncode == 0, text(r)) + device = listed_device(vc, volume) + if kernel_crypto and not device.startswith("/dev/mapper/"): + print("skip %s: kernel crypto not used (%s)" % (name, device)) + vc.dismount(mount_point) + return + pid = vc.service(volume) + busy = open(mount_point / "busy", "w") + try: + r = vc.run(["--non-interactive", "-d", "--emergency-unmount", + str(mount_point)]) + c.check("%s: emergency dismount" % name, r.returncode == 0, text(r)) + if not kernel_crypto: + c.check("%s: the old service is still running" % name, + pid is not None and os.path.exists("/proc/%d" % pid)) + r = mount(vc, volume, second_mount_point, kernel_crypto) + c.check("%s: mounting again while the volume is in use fails" % name, + r.returncode != 0 and "already mounted" in text(r), text(r)) + if r.returncode == 0: + vc.dismount(second_mount_point) + finally: + busy.close() + if not kernel_crypto: + c.check("%s: the old service exits once the file is closed" % name, + pid is None or wait_for_exit(pid, 30)) + end = time.monotonic() + 30 + while True: + r = mount(vc, volume, second_mount_point, kernel_crypto) + if r.returncode == 0 or time.monotonic() > end: + break + time.sleep(1) + c.check("%s: mounting again after the volume is released works" % name, + r.returncode == 0, text(r)) + vc.dismount(second_mount_point) + + +def test_other_path(vc, c, volume, mount_point, second_mount_point): + alias = vc.work / "alias.hc" + alias.symlink_to(volume) + r = mount(vc, volume, mount_point) + c.check("#1959: mount", r.returncode == 0, text(r)) + start = time.monotonic() + r = mount(vc, alias, second_mount_point) + elapsed = time.monotonic() - start + c.check("#1959: mounting through another path fails at once (%.1f s)" + % elapsed, + r.returncode != 0 and "already mounted" in text(r) + and elapsed < 5, text(r)) + c.check("#1959: dismount", vc.dismount_volumes([volume, alias])) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--veracrypt", + default=shutil.which("veracrypt") or "veracrypt") + args = parser.parse_args() + if os.geteuid() != 0: + sys.exit("Run as root: the test mounts volumes") + + work = Path(tempfile.mkdtemp(prefix="vc-regression-")) + vc = VeraCrypt(args.veracrypt, work) + c = Checks() + mount_points = [work / name for name in ("a", "b")[:2]] + for mount_point in mount_points: + mount_point.mkdir() + (work / "random").write_bytes(os.urandom(4096)) + volume = work / "a.hc" + try: + r = vc.create(volume, "10M", PASSWORD_A, "ext4") + c.check("create %s" % volume.name, r.returncode == 0, text(r)) + if c.failures: + sys.exit("Cannot create the test volume") + for kernel_crypto in (False, True): + test_detached_service(vc, c, volume, mount_points[0], + mount_points[1], kernel_crypto) + test_other_path(vc, c, volume, mount_points[0], mount_points[1]) + finally: + # Only the volumes of this test: unrelated ones may be mounted + if (not vc.dismount_volumes([volume, work / "alias.hc"]) + or any(os.path.ismount(m) for m in mount_points)): + sys.exit("A test volume is still mounted; not removing %s" % work) + shutil.rmtree(work, ignore_errors=True) + + if c.failures: + sys.exit("%d check(s) failed" % c.failures) + print("All checks passed") + + +if __name__ == "__main__": + main()