diff --git a/Tests/test_linux_loop_identity.py b/Tests/test_linux_loop_identity.py new file mode 100644 index 00000000..b04c5a93 --- /dev/null +++ b/Tests/test_linux_loop_identity.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python3 +"""Check that Linux dismounts, and rollbacks of failed mounts, act only on the volume's own devices. + +Usage: + sudo python3 Tests/test_linux_loop_identity.py --binary /path/to/veracrypt + +Requires root, util-linux, dmsetup and mkfs.ext4. Each check mounts a +disposable file container, with kernel cryptography (a device-mapper device +on a loop device attached to the container) and without it (a loop device +attached to the FUSE volume image), and checks that: + + - a dismount fails, and keeps the volume's devices, while its filesystem is + still mounted elsewhere (a bind mount or a mount in another mount + namespace), + - while another process has the volume's loop device open, a dismount fails + without kernel cryptography, as that device gives access to the decrypted + volume, and succeeds with it, the kernel detaching that device, which + gives access only to the container, once the process closes it, + - after a dismount failed with its devices already released, and their + names were reused by an unrelated loop device, filesystem and + device-mapper device, the listing does not show these as the volume's + and a retry releases the volume without touching them, + - a device-mapper device without a UUID named after a cascade layer that + the volume does not have, as left by a failed cascade mount of earlier + versions, does not fail the dismount and is left alone, + - emergency cleanup releases the volume's devices once their other users + are gone, + - a failed mount releases the devices it created, including the layers of a + cascade created before its top layer, and leaves alone a device-mapper + device of the same name that existed before, even one without a UUID, as + created by earlier versions, that maps the container, + - with kernel cryptography, the loop device is detached once the + device-mapper devices are released, even when they are removed by other + means, + - dismounting a volume hosted on a loop device leaves that device attached. + +With --legacy-binary, the path of an earlier version such as 1.26.29, it also +checks that volumes mounted by that version unmount and, as its +device-mapper devices have no UUID and are verified through the container, +that while the container is moved, a dismount fails, giving the cause, +without offering emergency cleanup, and keeps the volume's devices. + +Mounts are made in a private mount namespace. Artifacts are kept in a private +temporary directory. +""" + +import argparse +import ctypes +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import time +import traceback + +CLONE_NEWNS = 0x00020000 +TOOLS = ("dmsetup", "losetup", "mkfs.ext4", "mount", "umount", "unshare") +AUX_PREFIX = ".veracrypt_aux_mnt" + + +def mounts(): + with open("/proc/self/mounts") as f: + return [line.split()[1].replace("\\040", " ") for line in f] + + +def aux_mounts(): + return {m for m in mounts() if Path(m).name.startswith(AUX_PREFIX)} + + +def loop_backing(device): + try: + return Path("/sys/block", Path(device).name, "loop", "backing_file").read_text().strip() + except OSError: + return None + + +def loop_autoclear(device): + try: + return Path("/sys/block", Path(device).name, "loop", "autoclear").read_text().strip() == "1" + except OSError: + return None + + +def loop_devices(): + devices = {} + for loop in Path("/sys/block").glob("loop*"): + backing = loop_backing(loop.name) + if backing: + devices["/dev/" + loop.name] = backing + return devices + + +def dm_devices(): + devices = {} + for dm in Path("/sys/block").glob("dm-*"): + try: + devices[(dm / "dm" / "name").read_text().strip()] = (dm / "dm" / "uuid").read_text().strip() + except OSError: + pass + return devices + + +def wait(condition, seconds=10): + for _ in range(seconds * 2): + if condition(): + return True + time.sleep(0.5) + return condition() + + +class LoopIdentityChecks: + def __init__(self, args): + self.binary = str(Path(args.binary).resolve()) + self.legacy_binary = str(Path(args.legacy_binary).resolve()) if args.legacy_binary else None + self.slot = args.slot + self.dm_name = f"veracrypt{self.slot}" + self.root = Path(tempfile.mkdtemp(prefix="veracrypt-loop-identity-test-")).resolve() + self.mountpoint = self.root / "mount" + self.bind = self.root / "bind" + self.other = self.root / "other" + for d in (self.mountpoint, self.bind, self.other): + d.mkdir() + self.volume = self.root / "test.hc" + self.cascade = self.root / "cascade.hc" + self.other_image = self.root / "other.img" + self.password = "Disposable-loop-identity-test-only" + self.results = [] + self.failures = [] + self.processes = [] + self.mounted = None + # Devices of volumes mounted before the test + self.existing_aux = aux_mounts() + self.existing_loops = set(self.volume_loops()) + print("Artifacts:", self.root, flush=True) + + def record(self, entry): + self.results.append(entry) + (self.root / "results.json").write_text(json.dumps(self.results, indent=2)) + + def run(self, label, args, expected=0, stdin=None): + started = time.monotonic() + result = subprocess.run(list(map(str, args)), input=stdin, capture_output=True, text=True, timeout=300) + output = result.stdout + result.stderr + (self.root / (label + ".log")).write_text(output) + self.record(dict(label=label, returncode=result.returncode, + seconds=round(time.monotonic() - started, 3), output=output)) + if expected is not None and result.returncode != expected: + raise AssertionError(f"{label}: expected {expected}, got {result.returncode}: {output}") + return result + + def vc(self, label, *args, expected=0, binary=None): + return self.run(label, [binary or self.binary, "--text", "--non-interactive", *args], expected) + + def create(self, label, path, encryption): + self.vc(label, "--create", path, "--size=16M", "--volume-type=normal", "--encryption=" + encryption, + "--hash=SHA-512", "--filesystem=FAT", "--password=" + self.password, "--pim=1", "--keyfiles=", + "--random-source=/dev/urandom") + + def expect(self, label, passed, detail): + self.record(dict(label=label, passed=passed, detail=detail)) + print("PASS" if passed else "FAIL", label + ":", detail, flush=True) + if not passed: + self.failures.append(label) + + def listed(self): + result = self.vc("list", "--list", expected=None) + for line in result.stdout.splitlines(): + fields = line.split() + if fields[:1] == [f"{self.slot}:"]: + return dict(device=fields[2] if fields[2] != "-" else None, + mountpoint=fields[3] if len(fields) > 3 and fields[3] != "-" else None) + return None + + def volume_loops(self): + # Loop devices attached to a test container or to the volume image of an auxiliary mount, which is shown + # as /volume once that mount is detached + return [loop for loop, backing in loop_devices().items() + if backing in (str(self.volume), str(self.cascade), "/volume") + or Path(backing).parent.name.startswith(AUX_PREFIX)] + + def start(self, args, **kwargs): + process = subprocess.Popen(list(map(str, args)), **kwargs) + self.processes.append(process) + time.sleep(0.5) + return process + + def stop(self, process): + process.kill() + process.wait() + self.processes.remove(process) + time.sleep(0.5) + + def mount_args(self, path, kernel, *args): + return ("--mount", path, self.mountpoint, f"--slot={self.slot}", + "--mount-options=" + ("" if kernel else "nokernelcrypto"), "--password=" + self.password, + "--pim=1", "--keyfiles=", "--protect-hidden=no", *args) + + def mount(self, label, kernel, binary=None): + before = aux_mounts() + self.vc(label, *self.mount_args(self.volume, kernel), binary=binary) + self.mounted = self.volume + listed = self.listed() + device = listed and listed["device"] + if not device or device.startswith("/dev/mapper/") != kernel: + raise AssertionError(f"Unexpected device for the {'kernel' if kernel else 'FUSE'} mode: {listed}") + aux = list(aux_mounts() - before) + if len(aux) != 1: + raise AssertionError(f"Auxiliary mount not found: {aux}") + if kernel: + loop = self.run(label + "-loop", ["losetup", "-j", self.volume]).stdout.split(":")[0] + else: + loop = device + if not loop.startswith("/dev/loop"): + raise AssertionError(f"Loop device not found: {loop}") + return device, loop, aux[0] + + def mount_fails(self, label, kernel, *args, path=None): + path = path or self.volume + result = self.vc(label + "-mount", *self.mount_args(path, kernel, *args), expected=None) + if result.returncode == 0: + self.mounted = path + output = result.stdout + result.stderr + self.expect(label + "-mount-fails", result.returncode != 0, f"exit {result.returncode}: {output.strip()}") + return output + + def dismount(self, label, expected): + result = self.vc(label, "--unmount", self.volume, expected=None) + self.expect(label + "-" + ("succeeds" if expected else "fails"), (result.returncode == 0) == expected, + f"exit {result.returncode}: {(result.stdout + result.stderr).strip()}") + if result.returncode == 0: + self.mounted = None + return result.stdout + result.stderr + + def released(self, keep_loops=(), keep_dm=()): + state = {} + + def done(): + state.update(listed=self.listed(), aux_mounted=sorted(aux_mounts() - self.existing_aux), + loops=sorted(set(self.volume_loops()) - self.existing_loops - set(keep_loops)), + dm=sorted(n for n in dm_devices() if n.startswith(self.dm_name) and n not in keep_dm)) + return not any(state.values()) + + return (True, "volume and devices released") if wait(done) else (False, f"left: {state}") + + def check_busy(self, kind, kernel, hold): + mode = "kernel" if kernel else "fuse" + label = f"{mode}-{kind}" + device, loop, aux = self.mount(label + "-mount", kernel) + backing = loop_backing(loop) + holder = hold(device, loop) + if kernel and kind == "loop-open": + # The loop device gives access only to the container: the kernel detaches it once the holder closes it + self.dismount(label + "-dismount", True) + self.expect(label + "-volume-released", self.listed() is None and self.dm_name not in dm_devices(), + f"listed: {self.listed()}, dm: {dm_devices().get(self.dm_name)}") + self.expect(label + "-loop-autoclear", loop_backing(loop) == backing and loop_autoclear(loop) is True, + f"{loop}: {loop_backing(loop)}, autoclear {loop_autoclear(loop)}") + holder() + else: + output = self.dismount(label + "-dismount", False) + reason = "another process has the volume's loop device open" if kind == "loop-open" else "mounted elsewhere" + self.expect(label + "-cause-reported", reason in output, output.strip()) + listed = self.listed() + self.expect(label + "-still-listed", listed is not None, str(listed)) + # With kernel cryptography, the loop device is to be detached once device-mapper releases it + kept = loop_backing(loop) == backing and loop_autoclear(loop) is kernel + if kernel: + kept = kept and dm_devices().get(self.dm_name, "").startswith("VERACRYPT-") + self.expect(label + "-devices-kept", kept, f"{loop}: {loop_backing(loop)}, autoclear " + f"{loop_autoclear(loop)}, {self.dm_name}: {dm_devices().get(self.dm_name)}") + holder() + self.dismount(label + "-retry", True) + self.expect(label + "-released", *self.released()) + + def hold_bind(self, device, loop): + self.run("bind", ["mount", "--bind", self.mountpoint, self.bind]) + return lambda: self.run("unbind", ["umount", self.bind]) + + def hold_namespace(self, device, loop): + process = self.start(["unshare", "-m", "--propagation", "private", "sleep", "600"]) + return lambda: self.stop(process) + + def hold_loop(self, device, loop): + with open(loop, "rb") as f: + process = self.start(["sleep", "600"], stdin=f) + return lambda: self.stop(process) + + def check_reuse(self, kernel): + label = ("kernel" if kernel else "fuse") + "-reuse" + device, loop, aux = self.mount(label + "-mount", kernel) + holder = self.start(["sleep", "600"], cwd=aux) + self.dismount(label + "-dismount", False) + self.expect(label + "-loop-released", wait(lambda: loop_backing(loop) is None), f"{loop}: {loop_backing(loop)}") + + # Unrelated objects take over the released names + self.run(label + "-other-attach", ["losetup", loop, self.other_image]) + self.run(label + "-other-mount", ["mount", loop, self.other]) + foreign = f"FOREIGN-{os.getpid()}-{self.dm_name}" + if kernel: + self.run(label + "-other-dm", ["dmsetup", "create", self.dm_name, "--uuid", foreign, + "--table", "0 2048 zero"]) + try: + listed = self.listed() + self.expect(label + "-listing", listed == dict(device=None, mountpoint=None), str(listed)) + self.dismount(label + "-retry-busy", False) + self.stop(holder) + self.dismount(label + "-retry", True) + self.expect(label + "-other-mount-kept", str(self.other) in mounts(), f"{self.other} mounted") + self.expect(label + "-other-loop-kept", loop_backing(loop) == str(self.other_image), + f"{loop}: {loop_backing(loop)}") + if kernel: + self.expect(label + "-other-dm-kept", dm_devices().get(self.dm_name) == foreign, + f"{self.dm_name}: {dm_devices().get(self.dm_name)}") + finally: + self.run(label + "-other-unmount", ["umount", self.other], expected=None) + self.run(label + "-other-detach", ["losetup", "-d", loop], expected=None) + if kernel: + self.run(label + "-other-dm-remove", ["dmsetup", "remove", "--retry", self.dm_name], expected=None) + self.expect(label + "-released", *self.released()) + + def check_stale_layer(self, kernel): + # The test volume uses a single cipher, so it has no layer named _0 + label = ("kernel" if kernel else "fuse") + "-stale-layer" + stale = self.dm_name + "_0" + self.run(label + "-dm", ["dmsetup", "create", stale, "--table", "0 2048 zero"]) + try: + self.mount(label + "-mount", kernel) + self.dismount(label + "-dismount", True) + self.expect(label + "-dm-kept", dm_devices().get(stale) == "", f"{stale}: {dm_devices().get(stale)}") + self.expect(label + "-released", *self.released(keep_dm=[stale])) + finally: + self.run(label + "-dm-remove", ["dmsetup", "remove", "--retry", stale], expected=None) + + def check_emergency(self, kernel): + label = ("kernel" if kernel else "fuse") + "-emergency" + self.mount(label + "-mount", kernel) + process = self.start(["unshare", "-m", "--propagation", "private", "sleep", "600"]) + result = self.run(label + "-dismount", [self.binary, "--text", "--unmount", self.volume], + expected=None, stdin="y\n") + output = result.stdout + result.stderr + self.expect(label + "-cause-reported", "mounted elsewhere" in output, output.strip()) + self.expect(label + "-done", result.returncode == 0 and self.listed() is None, + f"exit {result.returncode}, listed: {self.listed()}") + if result.returncode == 0: + self.mounted = None + self.stop(process) + self.expect(label + "-released", *self.released()) + + def check_rollback(self, kernel): + mode = "kernel" if kernel else "fuse" + # Mounting the FAT filesystem as ext4 fails once the volume's devices are created + label = mode + "-rollback" + output = self.mount_fails(label, kernel, "--filesystem=ext4") + device = "/dev/mapper/" + self.dm_name if kernel else "/dev/loop" + self.expect(label + "-filesystem-mount-failed", device in output, output.strip()) + self.expect(label + "-released", *self.released()) + if not kernel: + return + + # Creating the device-mapper device fails if one of the same name exists, here without a UUID, as created + # by earlier versions, and mapping either something else or the container through another loop device + for kind in ("other", "container"): + label = f"kernel-rollback-existing-{kind}" + loop = None + try: + if kind == "container": + loop = self.run(label + "-attach", ["losetup", "-f", "--show", "-r", self.volume]).stdout.strip() + self.run(label + "-dm", ["dmsetup", "create", self.dm_name, "--table", + f"0 2048 linear {loop} 0" if loop else "0 2048 zero"]) + self.mount_fails(label, kernel) + self.expect(label + "-dm-kept", dm_devices().get(self.dm_name) == "", + f"{self.dm_name}: {dm_devices().get(self.dm_name)}") + if loop: + self.expect(label + "-loop-kept", loop_backing(loop) == str(self.volume), + f"{loop}: {loop_backing(loop)}") + self.expect(label + "-released", *self.released(keep_loops=[loop], keep_dm=[self.dm_name])) + finally: + self.run(label + "-dm-remove", ["dmsetup", "remove", "--retry", self.dm_name], expected=None) + if loop: + self.run(label + "-detach", ["losetup", "-d", loop], expected=None) + + def check_cascade_rollback(self): + # The top layer of a cascade cannot be created while a device of the same name exists, here without a + # UUID: the layer _0 created before it is removed, and that device is left alone + label = "kernel-cascade-rollback" + if not self.cascade.exists(): + self.create(label + "-create", self.cascade, "AES-Twofish") + self.run(label + "-dm", ["dmsetup", "create", self.dm_name, "--table", "0 2048 zero"]) + try: + output = self.mount_fails(label, True, path=self.cascade) + self.expect(label + "-top-layer-failed", f"create ioctl on {self.dm_name} " in output, output.strip()) + self.expect(label + "-dm-kept", dm_devices().get(self.dm_name) == "", + f"{self.dm_name}: {dm_devices().get(self.dm_name)}") + self.expect(label + "-released", *self.released(keep_dm=[self.dm_name])) + finally: + self.run(label + "-dm-remove", ["dmsetup", "remove", "--retry", self.dm_name], expected=None) + + def check_dm_removed(self): + # The loop device is detached once device-mapper releases it, even when its devices are removed by other means + label = "kernel-dm-removed" + device, loop, aux = self.mount(label + "-mount", True) + self.run(label + "-umount", ["umount", self.mountpoint]) + self.run(label + "-dm-remove", ["dmsetup", "remove", "--retry", self.dm_name]) + self.expect(label + "-loop-released", wait(lambda: loop_backing(loop) is None), f"{loop}: {loop_backing(loop)}") + self.dismount(label + "-dismount", True) + self.expect(label + "-released", *self.released()) + + def check_legacy(self, kernel): + # Device-mapper devices created by an earlier version have no UUID + label = ("kernel" if kernel else "fuse") + "-legacy" + self.mount(label + "-mount", kernel, self.legacy_binary) + self.dismount(label + "-dismount", True) + self.expect(label + "-released", *self.released()) + + def check_legacy_moved(self): + # A device-mapper device without a UUID is verified through the container + label = "kernel-legacy-moved" + device, loop, aux = self.mount(label + "-mount", True, self.legacy_binary) + moved = self.root / "moved.hc" + os.rename(self.volume, moved) + try: + output = self.dismount(label + "-dismount", False) + self.expect(label + "-cause-reported", "could not verify that this device belongs to the volume" in output + and str(self.volume) in output, output.strip()) + # Emergency cleanup is not offered: it would release the auxiliary mount, hiding the volume, but not its + # devices + result = self.run(label + "-interactive", [self.binary, "--text", "--unmount", self.volume], + expected=None, stdin="y\n") + self.expect(label + "-interactive-fails", result.returncode != 0, + f"exit {result.returncode}: {(result.stdout + result.stderr).strip()}") + listed = self.listed() + self.expect(label + "-still-listed", listed is not None, str(listed)) + mounted = str(self.mountpoint) in mounts() + self.expect(label + "-devices-kept", mounted and dm_devices().get(self.dm_name) == "" + and loop_backing(loop) == str(moved), f"{self.mountpoint} mounted: {mounted}, " + f"{self.dm_name}: {dm_devices().get(self.dm_name)!r}, {loop}: {loop_backing(loop)}") + finally: + os.rename(moved, self.volume) + self.dismount(label + "-retry", True) + self.expect(label + "-released", *self.released()) + + def check_device_hosted(self, kernel): + label = ("kernel" if kernel else "fuse") + "-device-hosted" + host = self.run(label + "-attach", ["losetup", "-f", "--show", self.volume]).stdout.strip() + try: + self.vc(label + "-mount", *self.mount_args(host, kernel)) + self.mounted = host + result = self.vc(label + "-dismount", "--unmount", host, expected=None) + if result.returncode == 0: + self.mounted = None + self.expect(label + "-dismount-succeeds", result.returncode == 0, + f"exit {result.returncode}: {(result.stdout + result.stderr).strip()}") + self.expect(label + "-host-kept", loop_backing(host) == str(self.volume) and loop_autoclear(host) is False, + f"{host}: {loop_backing(host)}, autoclear {loop_autoclear(host)}") + self.expect(label + "-released", *self.released(keep_loops=[host])) + finally: + if self.mounted: + self.vc(label + "-cleanup-unmount", "--force", "--unmount", host, expected=None) + self.mounted = None + self.run(label + "-detach", ["losetup", "-d", host], expected=None) + + def cleanup(self): + for process in list(self.processes): + self.stop(process) + for path in (self.bind, self.other): + if str(path) in mounts(): + self.run("cleanup-umount", ["umount", path], expected=None) + if self.mounted: + self.vc("cleanup-unmount", "--force", "--unmount", self.mounted, expected=None) + self.mounted = None + # Release devices left by a failed check, so that the next ones are not affected + for name in sorted(n for n in dm_devices() if n.startswith(self.dm_name)): + self.run("cleanup-dm-remove", ["dmsetup", "remove", "--retry", name], expected=None) + for loop in sorted(set(self.volume_loops()) - self.existing_loops): + self.run("cleanup-detach", ["losetup", "-d", loop], expected=None) + for aux in sorted(aux_mounts() - self.existing_aux): + self.run("cleanup-aux-unmount", ["umount", aux], expected=None) + + +def isolate_mounts(): + # Mount namespaces created by other processes during the test would copy + # its mounts and keep its devices busy. + libc = ctypes.CDLL(None, use_errno=True) + if libc.unshare(CLONE_NEWNS) != 0: + error = ctypes.get_errno() + raise OSError(error, "unshare: " + os.strerror(error)) + subprocess.run(["mount", "--make-rprivate", "/"], check=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--binary", required=True) + parser.add_argument("--legacy-binary", help="earlier version, such as 1.26.29, whose mounts are also checked") + parser.add_argument("--slot", type=int, default=62, help="free slot for the test volume (default: 62)") + parser.add_argument("--mode", choices=("fuse", "kernel"), action="append", + help="mount mode to check (default: both)") + args = parser.parse_args() + if not sys.platform.startswith("linux") or os.geteuid() != 0: + parser.error("Run on Linux as root") + missing = [tool for tool in TOOLS if not shutil.which(tool)] + if missing: + parser.error("Missing tools: " + ", ".join(missing)) + isolate_mounts() + checks = LoopIdentityChecks(args) + if checks.listed(): + parser.error(f"Slot {args.slot} is in use; choose a free one with --slot") + if any(name.startswith(checks.dm_name) for name in dm_devices()): + parser.error(f"/dev/mapper/{checks.dm_name} exists; choose a free slot with --slot") + + checks.create("create", checks.volume, "AES") + with open(checks.other_image, "wb") as f: + f.truncate(16 << 20) + checks.run("other-mkfs", ["mkfs.ext4", "-q", "-F", checks.other_image]) + + for mode in args.mode or ("fuse", "kernel"): + kernel = mode == "kernel" + mode_checks = [("bind", lambda: checks.check_busy("bind", kernel, checks.hold_bind)), + ("namespace", lambda: checks.check_busy("namespace", kernel, checks.hold_namespace)), + ("loop-open", lambda: checks.check_busy("loop-open", kernel, checks.hold_loop)), + ("reuse", lambda: checks.check_reuse(kernel)), + ("stale-layer", lambda: checks.check_stale_layer(kernel)), + ("emergency", lambda: checks.check_emergency(kernel)), + ("rollback", lambda: checks.check_rollback(kernel)), + ("device-hosted", lambda: checks.check_device_hosted(kernel))] + if kernel: + mode_checks += [("cascade-rollback", checks.check_cascade_rollback), + ("dm-removed", checks.check_dm_removed)] + if checks.legacy_binary: + mode_checks.append(("legacy", lambda: checks.check_legacy(kernel))) + if kernel: + mode_checks.append(("legacy-moved", checks.check_legacy_moved)) + for name, check in mode_checks: + try: + check() + except Exception as e: + traceback.print_exc() + checks.expect(f"{mode}-{name}-completed", False, f"{type(e).__name__}: {e}") + finally: + checks.cleanup() + if checks.failures: + sys.exit("Failed: " + ", ".join(checks.failures)) + print("All checks passed") + + +if __name__ == "__main__": + main() diff --git a/src/Common/Language.xml b/src/Common/Language.xml index ec52afc9..7f39ee3f 100644 --- a/src/Common/Language.xml +++ b/src/Common/Language.xml @@ -1666,7 +1666,7 @@ Mount NTFS volumes with an in-kernel Linux driver Linux only. When enabled and no explicit filesystem type was supplied, VeraCrypt probes the decrypted virtual device with blkid -p and mounts detected NTFS filesystems with an available in-kernel NTFS driver, bypassing mount helpers such as ntfs-3g. VeraCrypt uses ntfs when it is positively identified as a modern read/write driver or expected on Linux 7.1 or later, and otherwise uses ntfs3. If NTFS detection fails, VeraCrypt uses the normal automatic filesystem selection. If no supported in-kernel NTFS driver is available or loadable, mounting fails. This opt-in option can avoid suspend or hibernate hangs caused by frozen user-space FUSE filesystems. No supported in-kernel NTFS driver is available or loadable. To use the system default NTFS backend, disable the NTFS kernel-driver preference or do not request kernel NTFS explicitly. - Normal unmount of volume {0} failed. This can happen when applications still have files or directories open on the volume, or when the backing device was disconnected and the mount became stale.\n\nIf the device is still connected, choose No, close applications using the volume, and try unmounting again.\n\nIf the device was disconnected or the mount is stale, VeraCrypt can attempt emergency cleanup by lazy-detaching the filesystem and removing or scheduling removal of VeraCrypt kernel objects. Pending writes may have failed, data may be lost, and cleanup may remain pending until applications close open files. Check the filesystem with fsck or the appropriate repair tool before using it again.\n\nContinue? + Normal unmount of volume {0} failed. This can happen when applications still have files or directories open on the volume, when the volume's filesystem is also mounted elsewhere (for example, a bind mount or a mount in another mount namespace), when another process has the volume's device open, or when the backing device was disconnected and the mount became stale.\n\nIf the device is still connected, choose No, close applications using the volume, unmount any other mounts of its filesystem, and try unmounting again.\n\nIf the device was disconnected or the mount is stale, VeraCrypt can attempt emergency cleanup by lazy-detaching the filesystem and removing or scheduling removal of VeraCrypt kernel objects. Pending writes may have failed, data may be lost, and cleanup may remain pending until applications close open files. Check the filesystem with fsck or the appropriate repair tool before using it again.\n\nContinue? Emergency cleanup for volume {0} has been initiated. If the volume was disconnected, the mount was stale, or there were pending writes, check the filesystem with fsck or the appropriate repair tool before using it again. Creating volume data. Please wait. Finalizing volume creation: writing backup header. diff --git a/src/Core/Unix/CoreUnix.cpp b/src/Core/Unix/CoreUnix.cpp index 5525ab9c..8c63176b 100644 --- a/src/Core/Unix/CoreUnix.cpp +++ b/src/Core/Unix/CoreUnix.cpp @@ -306,8 +306,9 @@ namespace VeraCrypt #endif catch (NotApplicable &) { } - // Earlier versions recorded the host device of a device-hosted volume mounted with Linux kernel cryptography as its loop device - if (!mountedVolume->LoopDevice.IsEmpty() && wstring (mountedVolume->LoopDevice) != wstring (mountedVolume->Path)) + // On Linux, DismountNativeVolume detaches the loop device once it is verified to belong to the volume +#ifndef TC_LINUX + if (!mountedVolume->LoopDevice.IsEmpty()) { try { @@ -315,6 +316,7 @@ namespace VeraCrypt } catch (ExecutedProcessFailed&) { } } +#endif if (syncVolumeInfo || mountedVolume->Protection == VolumeProtection::HiddenVolumeReadOnly) { @@ -411,25 +413,6 @@ namespace VeraCrypt firstException.reset (e.CloneNew()); } - // Earlier versions recorded the host device of a device-hosted volume mounted with kernel cryptography as its loop device - if (!mountedVolume->LoopDevice.IsEmpty() && wstring (mountedVolume->LoopDevice) != wstring (mountedVolume->Path)) - { - try - { - DetachLoopDevice (mountedVolume->LoopDevice); - } - catch (ExecutedProcessFailed &e) - { - if (IsLoopDeviceAttached (mountedVolume->LoopDevice) && !firstException.get()) - firstException.reset (e.CloneNew()); - } - catch (Exception &e) - { - if (!firstException.get()) - firstException.reset (e.CloneNew()); - } - } - if (!mountedVolume->AuxMountPoint.IsEmpty()) { bool auxMountPointMounted = true; @@ -725,6 +708,9 @@ namespace VeraCrypt mountedVol->AuxMountPoint = mf.MountPoint; #ifndef TC_MACOSX + // Recorded devices may have been released and reused since + UpdateMountedVolumeInfo (mountedVol); + if (mountedVol->MountPoint.IsEmpty() && !mountedVol->VirtualDevice.IsEmpty()) { MountedFilesystemList mpl = GetMountedFilesystems (mountedVol->VirtualDevice); @@ -732,9 +718,6 @@ namespace VeraCrypt if (mpl.size() > 0) mountedVol->MountPoint = mpl.front()->MountPoint; } - - if (mountedVol->MountPoint.IsEmpty() || mountedVol->VirtualDevice.IsEmpty()) - UpdateMountedVolumeInfo (mountedVol); #endif volumes.push_back (mountedVol); diff --git a/src/Core/Unix/CoreUnix.h b/src/Core/Unix/CoreUnix.h index 42cc6688..a4c1238e 100644 --- a/src/Core/Unix/CoreUnix.h +++ b/src/Core/Unix/CoreUnix.h @@ -63,7 +63,6 @@ namespace VeraCrypt #ifdef TC_LINUX virtual void DismountFilesystemLazy (const DirectoryPath &mountPoint) const; virtual void DismountNativeVolumeDeferred (shared_ptr mountedVolume) const { DismountNativeVolume (mountedVolume); } - virtual bool IsLoopDeviceAttached (const DevicePath &devicePath) const { return devicePath.IsBlockDevice(); } #endif virtual bool FilesystemSupportsUnixPermissions (const DevicePath &devicePath) const; virtual string GetDefaultMountPointPrefix () const; diff --git a/src/Core/Unix/Linux/CoreLinux.cpp b/src/Core/Unix/Linux/CoreLinux.cpp index 922c65a4..a58f5738 100644 --- a/src/Core/Unix/Linux/CoreLinux.cpp +++ b/src/Core/Unix/Linux/CoreLinux.cpp @@ -11,12 +11,17 @@ */ #include +#include +#include #include #include #include #include #include #include +#include +#include +#include #include #include #include @@ -33,6 +38,577 @@ namespace VeraCrypt { + namespace + { + struct DeviceOwnership + { + enum Enum + { + Absent, + Owned, + Foreign + }; + }; + + struct LoopDetachResult + { + enum Enum + { + Released, + Claimed, // Exclusively claimed, e.g. by a mount of its filesystem + OpenElsewhere + }; + }; + + // Backing file of a loop device + struct LoopBacking + { + LoopBacking () : Device (0), Inode (0) { } + LoopBacking (const struct stat &fileStat) : Device ((uint64) fileStat.st_dev), Inode ((uint64) fileStat.st_ino) { } + + bool IsBacking (const loop_info64 &info) const { return info.lo_device == Device && info.lo_inode == Inode; } + bool IsFile (const struct stat &fileStat) const { return (uint64) fileStat.st_dev == Device && (uint64) fileStat.st_ino == Inode; } + + uint64 Device; + uint64 Inode; + }; + + struct DeviceMapperDevice + { + DeviceMapperDevice () : Number (0), Legacy (false) { } + + string KernelName; + string Name; + dev_t Number; + bool Legacy; + }; + + // Returns false if the attribute is gone, as when the device has been removed + bool ReadSysfsAttribute (const string &path, string &value) + { + value.clear(); + + int fd = open (path.c_str(), O_RDONLY | O_CLOEXEC); + if (fd == -1) + { + if (errno == ENOENT || errno == ENOTDIR || errno == ENODEV || errno == ENXIO) + return false; + throw SystemException (SRC_POS, path); + } + finally_do_arg (int, fd, { close (finally_arg); }); + + char buffer[4096]; + ssize_t size = read (fd, buffer, sizeof (buffer)); + if (size < 0) + { + if (errno == ENOENT || errno == ENODEV || errno == ENXIO) + return false; + throw SystemException (SRC_POS, path); + } + + value.assign (buffer, size); + size_t end = value.find ('\n'); + if (end != string::npos) + value.erase (end); + + return true; + } + + bool GetSysfsDeviceNumber (const string &sysfsDirectory, dev_t &deviceNumber) + { + string value; + if (!ReadSysfsAttribute (sysfsDirectory + "/dev", value)) + return false; + + vector fields = StringConverter::Split (value, ":"); + if (fields.size() != 2) + throw ParameterIncorrect (SRC_POS); + + deviceNumber = makedev (StringConverter::ToUInt32 (fields[0]), StringConverter::ToUInt32 (fields[1])); + return true; + } + + bool GetSysfsSlaves (const string &kernelName, list &slaves) + { + slaves.clear(); + + const string path = "/sys/class/block/" + kernelName + "/slaves"; + DIR *dir = opendir (path.c_str()); + if (!dir) + { + throw_sys_sub_if (errno != ENOENT && errno != ENOTDIR, path); + return false; + } + finally_do_arg (DIR *, dir, { closedir (finally_arg); }); + + struct dirent *entry; + while ((entry = readdir (dir)) != nullptr) + { + string name = entry->d_name; + if (name != "." && name != "..") + slaves.push_back (name); + } + + return true; + } + + // Returns false if the loop device is not bound + bool GetLoopDeviceStatus (int fd, const string &devicePath, loop_info64 &info) + { + Memory::Zero (&info, sizeof (info)); + if (ioctl (fd, LOOP_GET_STATUS64, &info) == 0) + return true; + + throw_sys_sub_if (errno != ENXIO, devicePath); + return false; + } + + bool IsLoopDevice (const struct stat &devStat) + { + return S_ISBLK (devStat.st_mode) && major (devStat.st_rdev) == LOOP_MAJOR; + } + + bool IsLoopDeviceBackedBy (const string &devicePath, const LoopBacking &backing) + { + struct stat devStat; + if (stat (devicePath.c_str(), &devStat) != 0) + { + throw_sys_sub_if (errno != ENOENT && errno != ENOTDIR, devicePath); + return false; + } + + if (!IsLoopDevice (devStat)) + return false; + + int fd = open (devicePath.c_str(), O_RDONLY | O_CLOEXEC); + if (fd == -1) + { + if (errno == ENOENT || errno == ENXIO || errno == ENODEV) + return false; + + throw_sys_sub_if ((errno != EACCES && errno != EPERM) || geteuid() == 0, devicePath); + + // Without access to the device, as when listing volumes unprivileged, compare the backing file path + stringstream sysfsPath; + sysfsPath << "/sys/dev/block/" << major (devStat.st_rdev) << ":" << minor (devStat.st_rdev) << "/loop/backing_file"; + + string backingFile; + struct stat backingStat; + return ReadSysfsAttribute (sysfsPath.str(), backingFile) + && stat (backingFile.c_str(), &backingStat) == 0 + && backing.IsFile (backingStat); + } + finally_do_arg (int, fd, { close (finally_arg); }); + + struct stat fdStat; + throw_sys_sub_if (fstat (fd, &fdStat) != 0, devicePath); + + loop_info64 info; + return IsLoopDevice (fdStat) && GetLoopDeviceStatus (fd, devicePath, info) && backing.IsBacking (info); + } + + // The descriptor keeps the device bound, so it cannot be released and reused in between + LoopDetachResult::Enum TryDetachLoopDevice (int fd, const string &devicePath, const LoopBacking &backing, bool deferred) + { + loop_info64 info; + if (!GetLoopDeviceStatus (fd, devicePath, info) || !backing.IsBacking (info)) + return LoopDetachResult::Released; + + if (ioctl (fd, LOOP_CLR_FD, 0) != 0) + { + if (errno == ENXIO) + return LoopDetachResult::Released; + + // Kernels before 3.2 refuse to detach a device in use + if (errno == EBUSY) + return LoopDetachResult::OpenElsewhere; + + throw SystemException (SRC_POS, devicePath); + } + + // With other openers, the kernel only marks the device for automatic detach at its last close + if (deferred) + return LoopDetachResult::Released; + + loop_info64 current; + if (!GetLoopDeviceStatus (fd, devicePath, current) || !backing.IsBacking (current)) + return LoopDetachResult::Released; + + // Keep the device until the volume can be dismounted completely, changing only the flag set above + if (!(info.lo_flags & LO_FLAGS_AUTOCLEAR)) + { + current.lo_flags &= ~(uint32) LO_FLAGS_AUTOCLEAR; + ioctl (fd, LOOP_SET_STATUS64, ¤t); + } + + return LoopDetachResult::OpenElsewhere; + } + + LoopDetachResult::Enum TryDetachLoopDevicePath (const string &devicePath, const string &backingFilePath, bool deferred) + { + struct stat devStat; + if (stat (devicePath.c_str(), &devStat) != 0) + { + throw_sys_sub_if (errno != ENOENT && errno != ENOTDIR, devicePath); + return LoopDetachResult::Released; + } + + if (!IsLoopDevice (devStat)) + return LoopDetachResult::Released; + + // An exclusive open fails while the filesystem is mounted anywhere, including in other mount namespaces + int fd = open (devicePath.c_str(), O_RDONLY | O_CLOEXEC | (deferred ? 0 : O_EXCL)); + if (fd == -1) + { + if (errno == ENOENT || errno == ENXIO || errno == ENODEV) + return LoopDetachResult::Released; + + throw_sys_sub_if (errno != EBUSY || deferred, devicePath); + + struct stat backingStat; + throw_sys_sub_if (stat (backingFilePath.c_str(), &backingStat) != 0, backingFilePath); + + return IsLoopDeviceBackedBy (devicePath, LoopBacking (backingStat)) ? LoopDetachResult::Claimed : LoopDetachResult::Released; + } + finally_do_arg (int, fd, { close (finally_arg); }); + + struct stat fdStat; + loop_info64 info; + throw_sys_sub_if (fstat (fd, &fdStat) != 0, devicePath); + + if (!IsLoopDevice (fdStat) || !GetLoopDeviceStatus (fd, devicePath, info)) + return LoopDetachResult::Released; + + struct stat backingStat; + throw_sys_sub_if (stat (backingFilePath.c_str(), &backingStat) != 0, backingFilePath); + + return TryDetachLoopDevice (fd, devicePath, LoopBacking (backingStat), deferred); + } + + void ThrowLoopDeviceBusy (const string &devicePath, LoopDetachResult::Enum result) + { + string reason = (result == LoopDetachResult::Claimed) + ? "the volume's filesystem may still be mounted elsewhere (for example, a bind mount or a mount in another mount namespace)." + : "another process has the volume's loop device open."; + + throw FilesystemDismountFailed (SRC_POS, "losetup", EBUSY, devicePath + ": device is busy: " + reason); + } + + // Detaches the loop device if it is still bound to the backing file + void DetachOwnedLoopDevice (const string &devicePath, const string &backingFilePath, bool deferred) + { + for (int t = 0; true; t++) + { + LoopDetachResult::Enum result = TryDetachLoopDevicePath (devicePath, backingFilePath, deferred); + if (result == LoopDetachResult::Released) + return; + + if (t > 5) + ThrowLoopDeviceBusy (devicePath, result); + + Thread::Sleep (200); + } + } + + // Has the kernel detach a loop device held open by the caller at its last close, which, while device-mapper + // holds it, comes once device-mapper releases it. Unlike LOOP_CLR_FD, which kernels before 3.2 refuse while + // the device is in use, this works with other openers on every kernel since 2.6.25. + bool SetLoopDeviceAutoClear (int fd) + { + loop_info64 info; + Memory::Zero (&info, sizeof (info)); + if (ioctl (fd, LOOP_GET_STATUS64, &info) != 0) + return false; + + if (info.lo_flags & LO_FLAGS_AUTOCLEAR) + return true; + + info.lo_flags |= LO_FLAGS_AUTOCLEAR; + return ioctl (fd, LOOP_SET_STATUS64, &info) == 0; + } + + bool IsBlockDeviceClaimed (const string &devicePath, dev_t deviceNumber) + { + struct stat devStat; + if (stat (devicePath.c_str(), &devStat) != 0 || !S_ISBLK (devStat.st_mode) || devStat.st_rdev != deviceNumber) + return false; + + int fd = open (devicePath.c_str(), O_RDONLY | O_CLOEXEC | O_EXCL); + if (fd == -1) + return errno == EBUSY; + + close (fd); + return false; + } + + // Device-mapper devices are tagged with the serial instance number of their mount + string GetDeviceMapperUuid (uint64 serialInstanceNumber, const string &name) + { + return "VERACRYPT-" + StringConverter::ToSingle (serialInstanceNumber) + "-" + name; + } + + // Earlier versions created devices without a UUID: their dependencies must lead to the volume + void VerifyLegacyDeviceMapperDevice (const VolumeInfo &volume, const string &name, const string &kernelName) + { + const string volumePath = volume.Path; + struct stat volumeStat; + const bool volumeFound = (stat (volumePath.c_str(), &volumeStat) == 0); + throw_sys_sub_if (!volumeFound && errno != ENOENT && errno != ENOTDIR, volumePath); + + string current = kernelName; + for (int depth = 0; volumeFound && depth < 8; depth++) + { + list slaves; + dev_t slaveNumber; + if (!GetSysfsSlaves (current, slaves) || slaves.size() != 1 + || !GetSysfsDeviceNumber ("/sys/class/block/" + slaves.front(), slaveNumber)) + { + break; + } + + const string &slave = slaves.front(); + + if (S_ISBLK (volumeStat.st_mode) && volumeStat.st_rdev == slaveNumber) + return; + + if (slave.find ("dm-") == 0) + { + current = slave; + continue; + } + + if (S_ISREG (volumeStat.st_mode) && major (slaveNumber) == LOOP_MAJOR) + { + const string loopPath = "/dev/" + slave; + struct stat loopStat; + + if (stat (loopPath.c_str(), &loopStat) == 0 && S_ISBLK (loopStat.st_mode) && loopStat.st_rdev == slaveNumber + && IsLoopDeviceBackedBy (loopPath, LoopBacking (volumeStat))) + { + return; + } + } + + break; + } + + // The device is left alone, and the dismount fails, until the volume is found where it was mounted from + errno = EPERM; + throw SystemException (SRC_POS, "/dev/mapper/" + name + ": could not verify that this device belongs to the volume. " + "Devices created by earlier versions of VeraCrypt are verified through the path of their volume: if the volume " + "has been moved, renamed or replaced, restore it to " + volumePath + " and try again."); + } + + // Devices without a UUID, created by earlier versions, are included only if requested + DeviceOwnership::Enum GetDeviceMapperOwnership (const VolumeInfo &volume, const string &name, bool includeLegacy, DeviceMapperDevice &device) + { + const string uuid = GetDeviceMapperUuid (volume.SerialInstanceNumber, name); + bool nameFound = false; + DeviceMapperDevice legacyDevice; + + 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 ("dm-") != 0) + continue; + + const string sysfsDirectory = "/sys/block/" + kernelName; + string deviceUuid, deviceName; + dev_t deviceNumber; + + if (!ReadSysfsAttribute (sysfsDirectory + "/dm/uuid", deviceUuid) + || !ReadSysfsAttribute (sysfsDirectory + "/dm/name", deviceName)) + { + continue; + } + + if (deviceUuid != uuid && (deviceName != name || !deviceUuid.empty() || !includeLegacy)) + { + if (deviceName == name) + nameFound = true; + continue; + } + + if (!GetSysfsDeviceNumber (sysfsDirectory, deviceNumber)) + continue; + + device.KernelName = kernelName; + device.Name = deviceName; + device.Number = deviceNumber; + device.Legacy = deviceUuid.empty(); + + if (!device.Legacy) + return DeviceOwnership::Owned; + + nameFound = true; + legacyDevice = device; + } + + if (!legacyDevice.KernelName.empty()) + { + VerifyLegacyDeviceMapperDevice (volume, name, legacyDevice.KernelName); + device = legacyDevice; + return DeviceOwnership::Owned; + } + + return nameFound ? DeviceOwnership::Foreign : DeviceOwnership::Absent; + } + + // Without kernel cryptography, the decrypted image /volume is attached to a loop device recorded as both devices + bool IsAuxVolumeImageLoopDevice (const VolumeInfo &volume) + { + return !volume.LoopDevice.IsEmpty() && wstring (volume.LoopDevice) == wstring (volume.VirtualDevice); + } + + string GetAuxVolumeImagePath (const VolumeInfo &volume) + { + return string (volume.AuxMountPoint) + FuseService::GetVolumeImagePath(); + } + + bool IsDeviceMapperDeviceOwned (const VolumeInfo &volume, const string &name, bool includeLegacy) + { + DeviceMapperDevice device; + return GetDeviceMapperOwnership (volume, name, includeLegacy, device) == DeviceOwnership::Owned; + } + + // Opens the loop device under a device-mapper device of the volume, or returns -1. The descriptor keeps + // the loop device bound once device-mapper releases it, so that it cannot be reused in between. + int OpenLoopDeviceUnder (const VolumeInfo &volume, const string &name) + { + DeviceMapperDevice device; + if (GetDeviceMapperOwnership (volume, name, true, device) != DeviceOwnership::Owned) + return -1; + + const string loopDevice = volume.LoopDevice; + int fd = open (loopDevice.c_str(), O_RDONLY | O_CLOEXEC); + if (fd == -1) + return -1; + + bool pinned = false; + finally_do_arg2 (int, fd, bool &, pinned, { if (!finally_arg2) close (finally_arg); }); + + // Once open, it must be the device held by the device-mapper device, which must still be the same + struct stat fdStat; + list slaves; + dev_t slaveNumber; + DeviceMapperDevice current; + + if (fstat (fd, &fdStat) != 0 || !IsLoopDevice (fdStat) + || !GetSysfsSlaves (device.KernelName, slaves) || slaves.size() != 1 + || !GetSysfsDeviceNumber ("/sys/class/block/" + slaves.front(), slaveNumber) || slaveNumber != fdStat.st_rdev + || GetDeviceMapperOwnership (volume, name, true, current) != DeviceOwnership::Owned + || current.KernelName != device.KernelName || current.Number != device.Number) + { + return -1; + } + + pinned = true; + return fd; + } + + void RemoveDeviceMapperDevice (const VolumeInfo &volume, const string &name, bool includeLegacy, bool deferred) + { + DeviceMapperDevice device; + if (GetDeviceMapperOwnership (volume, name, includeLegacy, device) != DeviceOwnership::Owned) + return; + + list deviceArgs; + if (device.Legacy) + { + deviceArgs.push_back (device.Name); + } + else + { + deviceArgs.push_back ("-u"); + deviceArgs.push_back (GetDeviceMapperUuid (volume.SerialInstanceNumber, name)); + } + + list removeArgs; + removeArgs.push_back ("remove"); + removeArgs.insert (removeArgs.end(), deviceArgs.begin(), deviceArgs.end()); + + bool removeNow = !deferred; + if (deferred) + { + list args; + args.push_back ("remove"); + args.push_back ("--retry"); + args.push_back ("--deferred"); + args.insert (args.end(), deviceArgs.begin(), deviceArgs.end()); + + try + { + Process::Execute ("dmsetup", args); + } + catch (ExecutedProcessFailed&) + { + if (IsDeviceMapperDeviceOwned (volume, name, includeLegacy)) + { + list fallbackArgs; + fallbackArgs.push_back ("remove"); + fallbackArgs.push_back ("--deferred"); + fallbackArgs.insert (fallbackArgs.end(), deviceArgs.begin(), deviceArgs.end()); + + try + { + Process::Execute ("dmsetup", fallbackArgs); + } + catch (ExecutedProcessFailed&) + { + removeNow = true; + } + } + } + } + + if (removeNow) + { + for (int t = 0; true; t++) + { + try + { + Process::Execute ("dmsetup", removeArgs); + break; + } + catch (ExecutedProcessFailed&) + { + if (!IsDeviceMapperDeviceOwned (volume, name, includeLegacy)) + break; + + if (t > 20) + { + const string devicePath = "/dev/mapper/" + device.Name; + if (IsBlockDeviceClaimed (devicePath, device.Number)) + { + throw FilesystemDismountFailed (SRC_POS, "dmsetup", EBUSY, devicePath + + ": device is busy: the volume's filesystem may still be mounted elsewhere (for example, a bind mount or a mount in another mount namespace)."); + } + + throw; + } + + Thread::Sleep (100); + } + } + } + + // Wait for the removal, unless it is deferred until the device is closed + for (int t = 0; t < 20; t++) + { + dev_t deviceNumber; + if (!GetSysfsDeviceNumber ("/sys/block/" + device.KernelName, deviceNumber) || deviceNumber != device.Number) + break; + + Thread::Sleep (100); + } + } + } + CoreLinux::CoreLinux () { } @@ -129,33 +705,28 @@ namespace VeraCrypt } } - bool CoreLinux::IsLoopDeviceAttached (const DevicePath &devicePath) const + void CoreLinux::CheckFilesystem (shared_ptr mountedVolume, bool repair) const { - struct stat statData; - if (stat (string (devicePath).c_str(), &statData) != 0 || !S_ISBLK (statData.st_mode)) - return false; + shared_ptr volume = ResolveMountedVolume (mountedVolume); - stringstream sysDevicePath; - sysDevicePath << "/sys/dev/block/" << major (statData.st_rdev) << ":" << minor (statData.st_rdev); - - if (FilesystemPath (sysDevicePath.str() + "/loop").IsDirectory()) - return true; - - list args; - args.push_back (devicePath); - - try + volume->VirtualDevice = GetOwnedVirtualDevice (*volume); + if (volume->VirtualDevice.IsEmpty()) { - Process::Execute ("losetup", args); - return true; + errno = ENXIO; + throw SystemException (SRC_POS, wstring (volume->Path)); } - catch (ExecutedProcessFailed&) - { - return false; - } - catch (...) { } - return true; + CoreUnix::CheckFilesystem (volume, repair); + } + + shared_ptr CoreLinux::DismountVolume (shared_ptr mountedVolume, bool ignoreOpenFiles, bool syncVolumeInfo) + { + return CoreUnix::DismountVolume (ResolveMountedVolume (mountedVolume), ignoreOpenFiles, syncVolumeInfo); + } + + shared_ptr CoreLinux::EmergencyDismountVolume (shared_ptr mountedVolume) + { + return CoreUnix::EmergencyDismountVolume (ResolveMountedVolume (mountedVolume)); } void CoreLinux::DismountNativeVolume (shared_ptr mountedVolume) const @@ -168,174 +739,161 @@ namespace VeraCrypt DismountNativeVolumeInternal (mountedVolume, true); } + // Releases only the objects that still belong to the volume, so that a retry after a partial dismount is safe void CoreLinux::DismountNativeVolumeInternal (shared_ptr mountedVolume, bool deferred) const { - string devPath = mountedVolume->VirtualDevice; - const string virtualDevice = devPath; + const VolumeInfo &volume = *mountedVolume; - if (devPath.find ("/dev/mapper/veracrypt") != 0) + if (IsAuxVolumeImageLoopDevice (volume)) + { + DetachOwnedLoopDevice (volume.LoopDevice, GetAuxVolumeImagePath (volume), deferred); + return; + } + + const string virtualDevice = volume.VirtualDevice; + const string mapperDirectory = "/dev/mapper/"; + + if (virtualDevice.find (mapperDirectory + "veracrypt") != 0) throw NotApplicable (SRC_POS); - // Deferred cleanup can follow a partial normal cleanup that removed an earlier mapper. - // Keep probing the suffixes VeraCrypt creates for cascade layers. - size_t maxDeviceMapperDeviceCount = 1; - if (deferred) + // Top device first, then the volume's cascade layers _0 ... _, any of which a partial dismount + // may have removed. A device without a UUID named after a layer that the volume does not have, as left by a + // failed cascade mount of an earlier version, cannot be verified and must not fail the dismount. + const string name = virtualDevice.substr (mapperDirectory.size()); + list names; + names.push_back (name); + + size_t cipherCount = 0; + size_t maxCipherCount = 1; + foreach (shared_ptr ea, EncryptionAlgorithm::GetAvailableAlgorithms()) { - foreach (shared_ptr ea, EncryptionAlgorithm::GetAvailableAlgorithms()) - { - size_t cipherCount = ea->GetCiphers().size(); - if (cipherCount > maxDeviceMapperDeviceCount) - maxDeviceMapperDeviceCount = cipherCount; - } + if (ea->GetName() == volume.EncryptionAlgorithmName) + cipherCount = ea->GetCiphers().size(); + + if (ea->GetCiphers().size() > maxCipherCount) + maxCipherCount = ea->GetCiphers().size(); } - size_t devCount = 0; - while (true) + // An unknown algorithm leaves all possible layers to check + if (cipherCount == 0) + cipherCount = maxCipherCount; + + for (size_t i = 0; i + 1 < cipherCount; i++) + names.push_back (name + "_" + StringConverter::ToSingle ((uint32) i)); + + // Device-hosted volumes have no loop device. Earlier versions recorded the host device instead. + const bool fileHosted = !volume.LoopDevice.IsEmpty() && wstring (volume.LoopDevice) != wstring (volume.Path); + + // Hold the loop device under the volume's device-mapper devices and, while they still hold it, have the kernel + // detach it at its last close, so that it cannot be released and reused in between. Another process holding + // it open, which sees only the container's ciphertext, does not fail the dismount. + int loopFd = -1; + finally_do_arg (int *, &loopFd, { if (*finally_arg != -1) close (*finally_arg); }); + + if (fileHosted) { - string deviceMapperName = StringConverter::Split (devPath, "/").back(); - bool devicePresent = deferred ? IsDeviceMapperDevicePresent (deviceMapperName) : FilesystemPath (devPath).IsBlockDevice(); - - if (!devicePresent) - { - if (deferred && devCount < maxDeviceMapperDeviceCount - 1) - { - devPath = virtualDevice + "_" + StringConverter::ToSingle (devCount++); - continue; - } - - break; - } - - list dmsetupArgs; - dmsetupArgs.push_back ("remove"); - if (deferred) - { - dmsetupArgs.push_back ("--retry"); - dmsetupArgs.push_back ("--deferred"); - } - dmsetupArgs.push_back (deviceMapperName); - - if (deferred) + for (list ::reverse_iterator i = names.rbegin(); i != names.rend() && loopFd == -1; ++i) { + // Failing to hold the loop device must not keep the device-mapper devices, which hold the volume's keys. + // One that cannot be verified makes the removal below fail. try { - Process::Execute ("dmsetup", dmsetupArgs); - } - catch (ExecutedProcessFailed&) - { - if (IsDeviceMapperDevicePresent (deviceMapperName)) - { - list deferredFallbackArgs; - deferredFallbackArgs.push_back ("remove"); - deferredFallbackArgs.push_back ("--deferred"); - deferredFallbackArgs.push_back (deviceMapperName); - - try - { - Process::Execute ("dmsetup", deferredFallbackArgs); - } - catch (ExecutedProcessFailed&) - { - if (IsDeviceMapperDevicePresent (deviceMapperName)) - { - list fallbackArgs; - fallbackArgs.push_back ("remove"); - fallbackArgs.push_back (deviceMapperName); - - for (int t = 0; true; t++) - { - try - { - Process::Execute ("dmsetup", fallbackArgs); - break; - } - catch (...) - { - if (!IsDeviceMapperDevicePresent (deviceMapperName)) - break; - - if (t > 20) - throw; - - Thread::Sleep (100); - } - } - } - } - } + loopFd = OpenLoopDeviceUnder (volume, *i); } + catch (...) { } } - else - { - for (int t = 0; true; t++) - { - try - { - Process::Execute ("dmsetup", dmsetupArgs); - break; - } - catch (...) - { - if (t > 20) - throw; - - Thread::Sleep (100); - } - } - } - - for (int t = 0; t < 20; t++) - { - if (deferred) - { - if (!IsDeviceMapperDevicePresent (deviceMapperName)) - break; - } - else if (!FilesystemPath (devPath).IsBlockDevice()) - break; - - Thread::Sleep (100); - } - - if (deferred && devCount >= maxDeviceMapperDeviceCount - 1) - break; - - devPath = virtualDevice + "_" + StringConverter::ToSingle (devCount++); } + + // Already set at mount time, except by earlier versions + if (loopFd != -1) + SetLoopDeviceAutoClear (loopFd); + + foreach (const string &deviceName, names) + RemoveDeviceMapperDevice (volume, deviceName, true, deferred); } - bool CoreLinux::IsDeviceMapperDevicePresent (const string &deviceMapperName) const + // Returns the virtual device if it still belongs to the volume + DevicePath CoreLinux::GetOwnedVirtualDevice (const VolumeInfo &volume) const { - if (deviceMapperName.empty()) - return false; + if (volume.VirtualDevice.IsEmpty()) + return DevicePath(); - DIR *sysBlockDir = opendir ("/sys/block"); - if (!sysBlockDir) - return FilesystemPath ("/dev/mapper/" + deviceMapperName).IsBlockDevice(); - - bool present = false; - struct dirent *entry; - while ((entry = readdir (sysBlockDir)) != nullptr) + if (IsAuxVolumeImageLoopDevice (volume)) { - string blockDeviceName = entry->d_name; - if (blockDeviceName.find ("dm-") != 0) - continue; + const string imagePath = GetAuxVolumeImagePath (volume); + struct stat imageStat; + throw_sys_sub_if (stat (imagePath.c_str(), &imageStat) != 0, imagePath); - try - { - TextReader tr ("/sys/block/" + blockDeviceName + "/dm/name"); - string name; - if (tr.ReadLine (name) && name == deviceMapperName) - { - present = true; - break; - } - } - catch (...) { } + return IsLoopDeviceBackedBy (volume.VirtualDevice, LoopBacking (imageStat)) ? volume.VirtualDevice : DevicePath(); } - closedir (sysBlockDir); - return present; + const string virtualDevice = volume.VirtualDevice; + const string mapperDirectory = "/dev/mapper/"; + + DeviceMapperDevice device; + if (virtualDevice.find (mapperDirectory + "veracrypt") == 0 + && GetDeviceMapperOwnership (volume, virtualDevice.substr (mapperDirectory.size()), true, device) == DeviceOwnership::Owned) + { + return DevicePath (mapperDirectory + device.Name); + } + + return DevicePath(); + } + + // The privileged service does not trust the volume information received from the client, which may be outdated + shared_ptr CoreLinux::ResolveMountedVolume (shared_ptr mountedVolume) const + { + if (!mountedVolume || mountedVolume->AuxMountPoint.IsEmpty()) + throw ParameterIncorrect (SRC_POS); + + const DirectoryPath auxMountPoint = mountedVolume->AuxMountPoint; + if (GetMountedFilesystems (DevicePath(), auxMountPoint).empty()) + throw MountServiceUnavailable (SRC_POS, wstring (auxMountPoint)); + + shared_ptr volume; + try + { + volume = ReadAuxiliaryVolumeInfo (auxMountPoint); + } + catch (...) + { + throw MountServiceUnavailable (SRC_POS, wstring (auxMountPoint)); + } + + if (volume->SerialInstanceNumber != mountedVolume->SerialInstanceNumber + || volume->SlotNumber != mountedVolume->SlotNumber + || wstring (volume->Path) != wstring (mountedVolume->Path)) + { + throw MountServiceUnavailable (SRC_POS, wstring (auxMountPoint)); + } + + // Unmount only a filesystem mounted from the volume's own virtual device + volume->MountPoint = DirectoryPath(); + try + { + DevicePath virtualDevice = GetOwnedVirtualDevice (*volume); + if (!virtualDevice.IsEmpty()) + { + MountedFilesystemList mountedFilesystems = GetMountedFilesystems (virtualDevice); + if (!mountedFilesystems.empty()) + volume->MountPoint = mountedFilesystems.front()->MountPoint; + } + } + catch (...) { } + + return volume; + } + + void CoreLinux::UpdateMountedVolumeInfo (shared_ptr mountedVolume) const + { + try + { + mountedVolume->VirtualDevice = GetOwnedVirtualDevice (*mountedVolume); + } + catch (...) + { + mountedVolume->VirtualDevice = DevicePath(); + } } HostDeviceList CoreLinux::GetHostDevices (bool pathListOnly) const @@ -524,15 +1082,56 @@ namespace VeraCrypt } bool loopDevAttached = false; - bool nativeDevCreated = false; bool filesystemMounted = false; - // Attach volume to loopback device if required + // Tags the device-mapper devices of this mount + const uint64 serialInstanceNumber = ReadAuxiliaryVolumeInfo (auxMountPoint)->SerialInstanceNumber; + + // Device-mapper devices whose creation was attempted, top layer first + list nativeDevNames; + + // Attach volume to loopback device if required. Through a descriptor held until the end of the mount, the loop + // device is verified and marked for the kernel to detach it once neither that descriptor nor device-mapper + // holds it: on failure, at dismount, and also if the device-mapper devices are removed by other means or this + // process is killed. Another device bound to the same name is never affected. VolumePath volumePath = volume->GetPath(); + int loopFd = -1; + finally_do_arg (int *, &loopFd, { if (*finally_arg != -1) close (*finally_arg); }); + if (!volumePath.IsDevice()) { + const string containerPath = volumePath; + struct stat containerStat; + throw_sys_sub_if (stat (containerPath.c_str(), &containerStat) != 0, containerPath); + volumePath = AttachFileToLoopDevice (volumePath, options.Protection == VolumeProtection::ReadOnly); loopDevAttached = true; + + const string loopDevice = volumePath; + loopFd = open (loopDevice.c_str(), O_RDONLY | O_CLOEXEC); + + bool verified = false; + try + { + struct stat fdStat; + loop_info64 info; + verified = loopFd != -1 && fstat (loopFd, &fdStat) == 0 && IsLoopDevice (fdStat) + && GetLoopDeviceStatus (loopFd, loopDevice, info) && LoopBacking (containerStat).IsBacking (info) + && SetLoopDeviceAutoClear (loopFd); + } + catch (...) { } + + if (!verified) + { + // Detach the loop device only if it is still bound to the container + try + { + TryDetachLoopDevicePath (loopDevice, containerPath, true); + } + catch (...) { } + + throw LoopDeviceSetupFailed (SRC_POS, wstring (volume->GetPath())); + } } string nativeDevPath; @@ -592,8 +1191,11 @@ namespace VeraCrypt execArgs.clear(); execArgs.push_back ("create"); + execArgs.push_back ("--uuid"); + execArgs.push_back (GetDeviceMapperUuid (serialInstanceNumber, nativeDevName.str())); execArgs.push_back (nativeDevName.str()); + nativeDevNames.push_front (nativeDevName.str()); Process::Execute ("dmsetup", execArgs, -1, nullptr, &dmCreateArgsBuf); // Wait for the device to be created @@ -613,7 +1215,6 @@ namespace VeraCrypt } } - nativeDevCreated = true; ++nativeDevCount; } @@ -666,23 +1267,20 @@ namespace VeraCrypt } catch (...) { } - try - { - if (nativeDevCreated) - { - make_shared_auto (VolumeInfo, vol); - vol->VirtualDevice = nativeDevPath; - DismountNativeVolume (vol); - } - } - catch (...) { } + // Release only the objects created above. The kernel detaches the loop device once device-mapper releases it + // and its descriptor is closed. Devices are removed by the UUID of this mount, so that one of the same name + // that existed before is left alone. + VolumeInfo createdVolume; + createdVolume.SerialInstanceNumber = serialInstanceNumber; - try + foreach (const string &name, nativeDevNames) { - if (loopDevAttached) - DetachLoopDevice (volumePath); + try + { + RemoveDeviceMapperDevice (createdVolume, name, false, false); + } + catch (...) { } } - catch (...) { } throw; } diff --git a/src/Core/Unix/Linux/CoreLinux.h b/src/Core/Unix/Linux/CoreLinux.h index 22978d3c..43da5133 100644 --- a/src/Core/Unix/Linux/CoreLinux.h +++ b/src/Core/Unix/Linux/CoreLinux.h @@ -24,6 +24,9 @@ namespace VeraCrypt CoreLinux (); virtual ~CoreLinux (); + virtual void CheckFilesystem (shared_ptr mountedVolume, bool repair = false) const; + 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; protected: @@ -32,13 +35,14 @@ namespace VeraCrypt virtual void DismountNativeVolume (shared_ptr mountedVolume) const; virtual void DismountNativeVolumeDeferred (shared_ptr mountedVolume) const; virtual MountedFilesystemList GetMountedFilesystems (const DevicePath &devicePath = DevicePath(), const DirectoryPath &mountPoint = DirectoryPath()) const; - virtual bool IsLoopDeviceAttached (const DevicePath &devicePath) const; virtual void MountFilesystem (const DevicePath &devicePath, const DirectoryPath &mountPoint, const string &filesystemType, bool readOnly, const string &systemMountOptions, bool internalMountOnly = false) const; virtual void MountVolumeNative (shared_ptr volume, MountOptions &options, const DirectoryPath &auxMountPoint) const; + virtual void UpdateMountedVolumeInfo (shared_ptr mountedVolume) const; private: void DismountNativeVolumeInternal (shared_ptr mountedVolume, bool deferred) const; - bool IsDeviceMapperDevicePresent (const string &deviceMapperName) const; + DevicePath GetOwnedVirtualDevice (const VolumeInfo &volume) const; + shared_ptr ResolveMountedVolume (shared_ptr mountedVolume) const; CoreLinux (const CoreLinux &); CoreLinux &operator= (const CoreLinux &); diff --git a/src/Main/UserInterface.cpp b/src/Main/UserInterface.cpp index 4d3fe1dc..4633bdaf 100644 --- a/src/Main/UserInterface.cpp +++ b/src/Main/UserInterface.cpp @@ -259,7 +259,7 @@ namespace VeraCrypt } } #ifdef TC_LINUX - catch (FilesystemDismountFailed&) + catch (FilesystemDismountFailed &e) { if (twoPassMode && firstPass) { @@ -278,7 +278,9 @@ namespace VeraCrypt } else if (interactive) { - if (AskYesNo (StringFormatter (LangString["LINUX_EMERGENCY_UNMOUNT_WARNING"], wstring (volume->Path)), false, true)) + // Show the cause of the failure with the warning + wxString warning = StringFormatter (LangString["LINUX_EMERGENCY_UNMOUNT_WARNING"], wstring (volume->Path)); + if (AskYesNo (ExceptionToMessage (e) + L"\n\n" + warning, false, true)) { { BusyScope busy (this);