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/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/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() diff --git a/src/Core/Unix/CoreUnix.cpp b/src/Core/Unix/CoreUnix.cpp index 6055c4c8e6..92cfb8ff6c 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,126 @@ namespace VeraCrypt throw MountPointNotAllowed (SRC_POS); } +#ifdef TC_LINUX + // 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) + { + 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.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; + 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."); + } + + 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) + { + 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; + } + + // 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) + { + // 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)) + lockName << "device-" << (uint64) volumeInfo.st_rdev; + else + lockName << "file-" << (uint64) volumeInfo.st_dev << "-" << (uint64) volumeInfo.st_ino; + + 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++) + { + const int error = errno; + if (error == EINTR) + continue; + + if (error != EWOULDBLOCK) + { + 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) + { + foreach_ref (const VolumeInfo &mounted, core.GetMountedVolumes()) + { + struct stat mountedInfo; + if (stat (string (mounted.Path).c_str(), &mountedInfo) == 0 && IsSameFile (mountedInfo, volumeInfo)) + throw VolumeAlreadyMounted (SRC_POS); + } + } + + // A service that has just been dismounted normally may still be exiting + 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) { CoalesceSlotNumberAndMountPoint (options); @@ -1301,6 +1423,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 (*this, *volume->GetFile(), volume->GetProtectionType() != VolumeProtection::ReadOnly); + 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/Core/Unix/CoreUnix.h b/src/Core/Unix/CoreUnix.h index 968a7c2d4d..5bc0e9f822 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,9 @@ 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; } + // 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 9f4651a006..4a43620bf8 100644 --- a/src/Core/Unix/Linux/CoreLinux.cpp +++ b/src/Core/Unix/Linux/CoreLinux.cpp @@ -718,6 +718,50 @@ 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)) + { + // 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()); + throw_sys_sub_if (!dir, holders.str()); + 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. + // A loop device that disappears meanwhile is not using it. + DIR *sysBlock = opendir ("/sys/block"); + 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 && IsLoopDeviceBackedBy ("/dev/" + kernelName, LoopBacking (volumeStat))) + return true; + } + + 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()) diff --git a/src/Platform/File.h b/src/Platform/File.h index 9c870c739c..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,11 +69,12 @@ 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 (); + SystemFileHandleType GetSystemHandle () const { return FileHandle; } void AssignSystemHandle (SystemFileHandleType openFileHandle, bool sharedHandle = true) { if (FileIsOpen) @@ -99,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; @@ -139,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;