mirror of
https://github.com/veracrypt/VeraCrypt.git
synced 2026-10-05 16:46:35 -05:00
macOS: isolate FUSE-T auxiliary mount paths
Create a fresh private parent for each FUSE-T auxiliary mount. Elevated parents stay root-owned and grant only the original user read/search access, restricting access through the auxiliary mount path and preventing caller-controlled path replacement during elevated setup. Clear inherited ACLs and reject temporary filesystems that ignore ownership. Remove per-mount parents on setup failure and service teardown, while preserving legacy temporary directories and discovery behavior. Add regression coverage for permissions, unique parent creation, rollback, and cleanup. FUSE-T transport authentication is outside this change.
This commit is contained in:
11 files changed
+442
-12
No files matched your search
@@ -14,8 +14,25 @@ build, then run:
|
|||||||
VC_TEST_BUILD_SRC=/absolute/path/to/build/src
|
VC_TEST_BUILD_SRC=/absolute/path/to/build/src
|
||||||
python3 Tests/test_macos_discovery.py --platform-archive "$VC_TEST_BUILD_SRC/Platform/Platform.a"
|
python3 Tests/test_macos_discovery.py --platform-archive "$VC_TEST_BUILD_SRC/Platform/Platform.a"
|
||||||
python3 Tests/test_fuset_cleanup.py --build-dir "$VC_TEST_BUILD_SRC"
|
python3 Tests/test_fuset_cleanup.py --build-dir "$VC_TEST_BUILD_SRC"
|
||||||
|
python3 Tests/test_fuset_aux_directory.py --build-dir "$VC_TEST_BUILD_SRC"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The auxiliary-directory check exercises the production FUSE-T private-parent
|
||||||
|
helpers against disposable local directories, including concurrent creation,
|
||||||
|
inherited ACL removal, setup failure cleanup, per-mount parent removal, and
|
||||||
|
discovery ownership. The temporary filesystem must support ownership and ACLs.
|
||||||
|
The helper check simulates disabled ownership without mounting a filesystem.
|
||||||
|
An optional elevated run also checks caller access, parent immutability, and
|
||||||
|
actual uid isolation using a harmless fixture:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo python3 Tests/test_fuset_aux_directory.py --build-dir "$VC_TEST_BUILD_SRC" --owner "$(id -u)"
|
||||||
|
```
|
||||||
|
|
||||||
|
The private parent protects auxiliary filesystem paths; these checks do not
|
||||||
|
assess FUSE-T transport authentication. Existing volumes must be dismounted and
|
||||||
|
remounted with the updated build to receive this protection.
|
||||||
|
|
||||||
The discovery runner compiles the production plist and batch-refresh methods
|
The discovery runner compiles the production plist and batch-refresh methods
|
||||||
with an in-memory inventory provider. It checks exact and legacy alias matches,
|
with an in-memory inventory provider. It checks exact and legacy alias matches,
|
||||||
unresolved inventory, stale-field invalidation, one query per refresh, and fresh
|
unresolved inventory, stale-field invalidation, one query per refresh, and fresh
|
||||||
|
|||||||
@@ -93,6 +93,17 @@ static int test_stat (const char *path, struct stat *value)
|
|||||||
return stat (path, value);
|
return stat (path, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static char *test_mkdtemp (char *path)
|
||||||
|
{
|
||||||
|
if (mode_is ("aux-child") && fixture_path (path, "/.veracrypt_aux_mnt-XXXXXXXXXXXX"))
|
||||||
|
{
|
||||||
|
mark_fault();
|
||||||
|
errno = EACCES;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return mkdtemp (path);
|
||||||
|
}
|
||||||
|
|
||||||
static unsigned fault_delay (void)
|
static unsigned fault_delay (void)
|
||||||
{
|
{
|
||||||
const char *delay = getenv ("VC_FUSET_TEST_DELAY");
|
const char *delay = getenv ("VC_FUSET_TEST_DELAY");
|
||||||
@@ -136,5 +147,6 @@ static int test_unlinkat (int fd, const char *path, int flags)
|
|||||||
INTERPOSE (test_connect, connect);
|
INTERPOSE (test_connect, connect);
|
||||||
INTERPOSE (test_open, open);
|
INTERPOSE (test_open, open);
|
||||||
INTERPOSE (test_stat, stat);
|
INTERPOSE (test_stat, stat);
|
||||||
|
INTERPOSE (test_mkdtemp, mkdtemp);
|
||||||
INTERPOSE (test_unmount, unmount);
|
INTERPOSE (test_unmount, unmount);
|
||||||
INTERPOSE (test_unlinkat, unlinkat);
|
INTERPOSE (test_unlinkat, unlinkat);
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test FUSE-T's production private-parent helpers using disposable directories.
|
||||||
|
|
||||||
|
Requires a matching macOS build, but no mounted volumes or FUSE service. Optional
|
||||||
|
--owner UID, when run as root, checks elevated access and real uid isolation
|
||||||
|
using a harmless sentinel file. No user accounts are created or modified.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import concurrent.futures
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def function(source, signature):
|
||||||
|
begin = source.index("\t" + signature)
|
||||||
|
return source[begin:source.index("\n\t}", begin) + 3]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--build-dir", type=Path, required=True)
|
||||||
|
parser.add_argument("--owner", type=int, default=os.getuid())
|
||||||
|
args = parser.parse_args()
|
||||||
|
if sys.platform != "darwin":
|
||||||
|
parser.error("macOS is required")
|
||||||
|
if args.owner != os.getuid() and os.geteuid() != 0:
|
||||||
|
parser.error("--owner requires root when it differs from the current uid")
|
||||||
|
owner = args.owner
|
||||||
|
elevated = os.geteuid() == 0
|
||||||
|
prefix = ".veracrypt_aux_root_" if elevated else ".veracrypt_aux_"
|
||||||
|
other = 502 if owner != 502 else 503
|
||||||
|
with tempfile.TemporaryDirectory(prefix="vc-aux-directory-", dir="/private/tmp") as temporary:
|
||||||
|
work = Path(temporary)
|
||||||
|
work.chmod(0o755)
|
||||||
|
source = (ROOT / "src/Core/Unix/CoreUnix.cpp").read_text()
|
||||||
|
helpers = function(source, "static string CreateFuseTAuxiliaryDirectory")
|
||||||
|
helpers += function(source, "static bool IsOtherUsersFuseTAuxiliaryMount")
|
||||||
|
service = (ROOT / "src/Driver/Fuse/FuseService.cpp").read_text()
|
||||||
|
helpers += function(service, "void FuseService::RemoveAuxMountParent")
|
||||||
|
begin = service.index("\tclass FuseServiceAuxDirectory")
|
||||||
|
helpers += service[begin:service.index("\n\t};", begin) + 4]
|
||||||
|
unit = work / "check.cpp"
|
||||||
|
unit.write_text(r'''
|
||||||
|
#include "Core/CoreException.h"
|
||||||
|
#include "Core/Unix/MountedFilesystem.h"
|
||||||
|
#include "Platform/StringConverter.h"
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <membership.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <sys/acl.h>
|
||||||
|
#include <sys/mount.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <iostream>
|
||||||
|
namespace VeraCrypt {
|
||||||
|
static bool IgnoreOwnership=false, FailAcl=false, Mounted=false;
|
||||||
|
static int TestStatfs(int fd, struct statfs *info) {
|
||||||
|
int result=fstatfs(fd, info);
|
||||||
|
if(result==0 && IgnoreOwnership)info->f_flags |= MNT_IGNORE_OWNERSHIP;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
static int TestSetAcl(int fd, acl_t acl, acl_type_t type) {
|
||||||
|
if(FailAcl){errno=ENOTSUP;return -1;}
|
||||||
|
return acl_set_fd_np(fd, acl, type);
|
||||||
|
}
|
||||||
|
static bool fuse_service_find_mount(const char *, fsid_t &) { return Mounted; }
|
||||||
|
struct FuseService { static void RemoveAuxMountParent(const string &, int=-1); };
|
||||||
|
#define fstatfs TestStatfs
|
||||||
|
#define acl_set_fd_np TestSetAcl
|
||||||
|
''' + helpers + r'''
|
||||||
|
}
|
||||||
|
using namespace VeraCrypt;
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
try {
|
||||||
|
const std::string command=argv[1];
|
||||||
|
if(command=="create" || command=="noowners" || command=="acl-failure") {
|
||||||
|
IgnoreOwnership=command=="noowners"; FailAcl=command=="acl-failure";
|
||||||
|
std::cout << CreateFuseTAuxiliaryDirectory(argv[2], std::stoul(argv[3])) << '\n';
|
||||||
|
} else if(command=="cleanup") {
|
||||||
|
FuseService::RemoveAuxMountParent(argv[2]);
|
||||||
|
} else if(command.find("service-")==0) {
|
||||||
|
Mounted=command=="service-mounted";
|
||||||
|
FuseServiceAuxDirectory directory(argv[2]);
|
||||||
|
if(command=="service-removed" && rmdir(argv[2])!=0)throw std::runtime_error("fixture removal failed");
|
||||||
|
if(command=="service-replaced-child" || command=="service-replaced-parent") {
|
||||||
|
std::string path=argv[2];
|
||||||
|
if(command=="service-replaced-parent")path=path.substr(0, path.find_last_of('/'));
|
||||||
|
if(rename(path.c_str(), (path+".original").c_str())!=0 || mkdir(path.c_str(), 0700)!=0)
|
||||||
|
throw std::runtime_error("fixture replacement failed");
|
||||||
|
}
|
||||||
|
} else if(command=="filter") {
|
||||||
|
MountedFilesystem mount;
|
||||||
|
mount.MountPoint = argv[2]; mount.Owner = std::stoul(argv[3]); mount.Type = argv[4];
|
||||||
|
std::cout << IsOtherUsersFuseTAuxiliaryMount(mount, std::stoul(argv[5]), std::stoul(argv[6])) << '\n';
|
||||||
|
}
|
||||||
|
} catch (std::exception &e) { std::cerr << e.what() << '\n'; return 1; }
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
binary = work / "check"
|
||||||
|
subprocess.run(["clang++", "-std=c++11", "-DTC_UNIX", "-DTC_MACOSX", "-I" + str(ROOT / "src"),
|
||||||
|
str(unit), str(args.build_dir / "Core/Core.a"),
|
||||||
|
str(args.build_dir / "Platform/Platform.a"), "-Wl,-dead_strip", "-o", str(binary)], check=True)
|
||||||
|
|
||||||
|
def create(base, uid=owner, succeeds=True, command="create"):
|
||||||
|
result = subprocess.run([str(binary), command, str(base), str(uid)], capture_output=True, text=True)
|
||||||
|
assert (result.returncode == 0) == succeeds, result.stdout + result.stderr
|
||||||
|
return Path(result.stdout.strip()) if succeeds else None
|
||||||
|
|
||||||
|
def case(name):
|
||||||
|
base = work / name
|
||||||
|
base.mkdir(mode=0o755)
|
||||||
|
return base
|
||||||
|
|
||||||
|
base = case("normal")
|
||||||
|
directory = create(base)
|
||||||
|
info = directory.stat()
|
||||||
|
assert info.st_uid == (0 if elevated else owner) and stat.S_IMODE(info.st_mode) == 0o700
|
||||||
|
assert create(base) != directory
|
||||||
|
sentinel = directory / "sentinel"
|
||||||
|
sentinel.write_text("private directory regression fixture\n")
|
||||||
|
if os.geteuid() == 0:
|
||||||
|
os.chown(sentinel, owner, -1)
|
||||||
|
sentinel.chmod(0o644)
|
||||||
|
|
||||||
|
def foreign(path=directory, user=other, real=other, mount_owner=0, backend="smbfs"):
|
||||||
|
result = subprocess.check_output([str(binary), "filter", str(path / ".veracrypt_aux_mnt-unit"),
|
||||||
|
str(mount_owner), backend, str(user), str(real)], text=True)
|
||||||
|
return result.strip() == "1"
|
||||||
|
|
||||||
|
assert foreign() == elevated # Only the elevated parent name identifies the user.
|
||||||
|
assert not foreign(user=0, real=other)
|
||||||
|
assert not foreign(user=owner, real=owner)
|
||||||
|
assert not foreign(user=0, real=owner)
|
||||||
|
assert not foreign(path=base) # Legacy mounts in a shared temp directory.
|
||||||
|
legacy = case("legacy-acl")
|
||||||
|
legacy.chmod(0o700)
|
||||||
|
subprocess.run(["chmod", "+a", "everyone allow search", str(legacy)], check=True)
|
||||||
|
assert not foreign(path=legacy) # ACL access is not reflected in owner/mode bits.
|
||||||
|
assert not foreign(backend="macfuse")
|
||||||
|
assert not foreign(mount_owner=owner if owner else other)
|
||||||
|
assert foreign(path=work / f".veracrypt_aux_root_{owner}")
|
||||||
|
assert not foreign(path=work / f".veracrypt_aux_root_{owner}", user=owner, real=owner)
|
||||||
|
assert not foreign(path=work / ".veracrypt_aux_root_invalid-ABCDEFGHIJKL")
|
||||||
|
|
||||||
|
for kind in ("symlink", "file", "directory"):
|
||||||
|
base = case(kind)
|
||||||
|
path = base / f"{prefix}{owner}"
|
||||||
|
if kind == "symlink":
|
||||||
|
path.symlink_to(directory, target_is_directory=True)
|
||||||
|
elif kind == "file":
|
||||||
|
path.write_text("unchanged")
|
||||||
|
else:
|
||||||
|
path.mkdir(mode=0o700)
|
||||||
|
before = path.lstat()
|
||||||
|
assert create(base) != path
|
||||||
|
after = path.lstat()
|
||||||
|
assert (before.st_ino, before.st_uid, before.st_mode) == (after.st_ino, after.st_uid, after.st_mode)
|
||||||
|
assert sentinel.read_text() == "private directory regression fixture\n"
|
||||||
|
|
||||||
|
base = case("inherited-acl")
|
||||||
|
subprocess.run(["chmod", "+a", "everyone allow read,search,directory_inherit", str(base)], check=True)
|
||||||
|
inherited = create(base)
|
||||||
|
acl = subprocess.check_output(["ls", "-lde", str(inherited)], text=True)
|
||||||
|
entries = re.findall(r"^\s*\d+: (.+)$", acl, re.M)
|
||||||
|
assert len(entries) == (1 if elevated and owner else 0), acl
|
||||||
|
if entries:
|
||||||
|
assert entries[0].endswith(" allow list,search,readattr,readsecurity"), acl
|
||||||
|
|
||||||
|
for failure in ("noowners", "acl-failure"):
|
||||||
|
base = case(failure)
|
||||||
|
create(base, succeeds=False, command=failure)
|
||||||
|
assert not list(base.iterdir()), "Failed parent setup left a directory"
|
||||||
|
|
||||||
|
base = case("concurrent")
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=12) as pool:
|
||||||
|
paths = list(pool.map(lambda _: create(base), range(24)))
|
||||||
|
assert len(set(paths)) == 24 and set(base.iterdir()) == set(paths)
|
||||||
|
for parent in paths:
|
||||||
|
child = parent / ".veracrypt_aux_mnt-unit"
|
||||||
|
child.mkdir()
|
||||||
|
subprocess.run([str(binary), "service-mounted", str(child)], check=True)
|
||||||
|
assert child.exists() and parent.exists()
|
||||||
|
subprocess.run([str(binary), "service-cleanup", str(child)], check=True)
|
||||||
|
assert not child.exists() and not parent.exists()
|
||||||
|
# An older client can remove the child before its service exits.
|
||||||
|
parent = create(base)
|
||||||
|
child = parent / ".veracrypt_aux_mnt-unit"
|
||||||
|
child.mkdir()
|
||||||
|
subprocess.run([str(binary), "service-removed", str(child)], check=True)
|
||||||
|
assert not parent.exists()
|
||||||
|
for target in ("child", "parent"):
|
||||||
|
parent = create(base)
|
||||||
|
child = parent / ".veracrypt_aux_mnt-unit"
|
||||||
|
child.mkdir()
|
||||||
|
subprocess.run([str(binary), "service-replaced-" + target, str(child)], check=True)
|
||||||
|
assert parent.exists(), "Removed a replacement parent"
|
||||||
|
if target == "child":
|
||||||
|
assert child.exists(), "Removed a replacement child"
|
||||||
|
# The fallback removes only empty parents in the per-mount format.
|
||||||
|
parent = create(base)
|
||||||
|
subprocess.run([str(binary), "cleanup", str(parent / ".veracrypt_aux_mnt-unit")], check=True)
|
||||||
|
assert not parent.exists()
|
||||||
|
for name in ("legacy-tmp", f"{prefix}{owner}", f"{prefix}{owner}-invalid"):
|
||||||
|
parent = base / name
|
||||||
|
parent.mkdir()
|
||||||
|
child = parent / ".veracrypt_aux_mnt-unit"
|
||||||
|
child.mkdir()
|
||||||
|
subprocess.run([str(binary), "service-cleanup", str(child)], check=True)
|
||||||
|
assert parent.exists() and not child.exists()
|
||||||
|
print("PASS: unique parents, owner/mode, ACL normalization, setup rollback, discovery, legacy preservation, service cleanup")
|
||||||
|
|
||||||
|
if os.geteuid() == 0 and owner != 0:
|
||||||
|
for uid, allowed in ((owner, True), (other, False)):
|
||||||
|
pid = os.fork()
|
||||||
|
if pid == 0:
|
||||||
|
try:
|
||||||
|
os.setgroups([])
|
||||||
|
os.setgid(20)
|
||||||
|
os.setuid(uid)
|
||||||
|
try:
|
||||||
|
with sentinel.open("rb") as stream:
|
||||||
|
stream.read(1)
|
||||||
|
assert allowed
|
||||||
|
except PermissionError:
|
||||||
|
assert not allowed
|
||||||
|
# Read/search access must not allow path replacement.
|
||||||
|
try:
|
||||||
|
(directory / "replacement").mkdir()
|
||||||
|
raise AssertionError("caller can modify the elevated parent")
|
||||||
|
except PermissionError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
directory.chmod(0o777)
|
||||||
|
raise AssertionError("caller can change the elevated parent's permissions")
|
||||||
|
except PermissionError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
directory.rename(directory.with_name("replacement-parent"))
|
||||||
|
raise AssertionError("caller can replace the elevated parent")
|
||||||
|
except PermissionError:
|
||||||
|
pass
|
||||||
|
# Discovery also works without entering the parent.
|
||||||
|
expected = "0" if allowed else "1"
|
||||||
|
output = subprocess.check_output([str(binary), "filter",
|
||||||
|
str(directory / ".veracrypt_aux_mnt-unit"),
|
||||||
|
"0", "smbfs", str(uid), str(uid)], text=True)
|
||||||
|
assert output.strip() == expected
|
||||||
|
os._exit(0)
|
||||||
|
except BaseException:
|
||||||
|
os._exit(1)
|
||||||
|
_, status = os.waitpid(pid, 0)
|
||||||
|
assert os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0, (uid, status)
|
||||||
|
print("PASS: root owns elevated parent; caller has read/search only; unrelated uid gets EACCES; discovery scope")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -263,6 +263,7 @@ struct FuseService {
|
|||||||
if(FailWait)throw DismountServiceCleanupFailed("mock timeout", L"pid=1234, auxiliary mount=/in-memory");
|
if(FailWait)throw DismountServiceCleanupFailed("mock timeout", L"pid=1234, auxiliary mount=/in-memory");
|
||||||
}
|
}
|
||||||
static void DismountLegacy(const DismountRequest &) { throw std::runtime_error("unexpected legacy path"); }
|
static void DismountLegacy(const DismountRequest &) { throw std::runtime_error("unexpected legacy path"); }
|
||||||
|
static void RemoveAuxMountParent(const std::string &) { throw std::runtime_error("unexpected directory cleanup"); }
|
||||||
};
|
};
|
||||||
struct Process {
|
struct Process {
|
||||||
static std::string Execute(const std::string &,const std::list<std::string> &) { ++Commands; throw std::runtime_error("unexpected disk command"); }
|
static std::string Execute(const std::string &,const std::list<std::string> &) { ++Commands; throw std::runtime_error("unexpected disk command"); }
|
||||||
|
|||||||
@@ -81,11 +81,16 @@ class DismountChecks:
|
|||||||
self.vc(label, "--mount", self.volume, self.mountpoint,
|
self.vc(label, "--mount", self.volume, self.mountpoint,
|
||||||
"--password=" + self.password, "--pim=1", "--keyfiles=",
|
"--password=" + self.password, "--pim=1", "--keyfiles=",
|
||||||
"--protect-hidden=no", *options, binary=binary)
|
"--protect-hidden=no", *options, binary=binary)
|
||||||
auxiliaries = [p.parent for p in self.tmpdir.glob(".veracrypt_aux_mnt*/control")]
|
auxiliaries = [p.parent for p in self.tmpdir.glob("**/.veracrypt_aux_mnt*/control")]
|
||||||
if len(auxiliaries) != 1:
|
if len(auxiliaries) != 1:
|
||||||
raise AssertionError(f"Expected one test service, found {auxiliaries}")
|
raise AssertionError(f"Expected one test service, found {auxiliaries}")
|
||||||
aux = auxiliaries[0]
|
aux = auxiliaries[0]
|
||||||
self.active.update(aux=aux)
|
self.active.update(aux=aux)
|
||||||
|
if binary == self.binary:
|
||||||
|
parent = aux.parent.stat()
|
||||||
|
if (not re.fullmatch(rf"\.veracrypt_aux_{os.getuid()}-[A-Za-z0-9]{{12}}", aux.parent.name)
|
||||||
|
or parent.st_uid != os.getuid() or parent.st_mode & 0o777 != 0o700):
|
||||||
|
raise AssertionError("Auxiliary mount lacks a private parent owned by the caller")
|
||||||
identity = aux / "shutdown"
|
identity = aux / "shutdown"
|
||||||
if identity.exists():
|
if identity.exists():
|
||||||
pid, serial, slot = map(int, identity.read_text().split())
|
pid, serial, slot = map(int, identity.read_text().split())
|
||||||
@@ -173,8 +178,9 @@ class DismountChecks:
|
|||||||
endpoint = self.active.get("endpoint")
|
endpoint = self.active.get("endpoint")
|
||||||
state = dict(label=label, mounts=self.mounted_paths(), handles=handles.stdout,
|
state = dict(label=label, mounts=self.mounted_paths(), handles=handles.stdout,
|
||||||
service_alive=alive, endpoint_exists=bool(endpoint and endpoint.exists()),
|
service_alive=alive, endpoint_exists=bool(endpoint and endpoint.exists()),
|
||||||
backend=backend, services=services, images=images)
|
backend=backend, services=services, images=images,
|
||||||
if not any(state[key] for key in ("mounts", "handles", "service_alive", "endpoint_exists", "backend", "services", "images")):
|
auxiliary_parents=[str(p) for p in self.tmpdir.glob(f".veracrypt_aux_{os.getuid()}-*")])
|
||||||
|
if not any(state[key] for key in ("mounts", "handles", "service_alive", "endpoint_exists", "backend", "services", "images", "auxiliary_parents")):
|
||||||
self.record(state)
|
self.record(state)
|
||||||
self.active = None
|
self.active = None
|
||||||
return
|
return
|
||||||
@@ -320,7 +326,7 @@ class DismountChecks:
|
|||||||
source = Path(__file__).with_name("fuset_startup_faults.c")
|
source = Path(__file__).with_name("fuset_startup_faults.c")
|
||||||
self.run("build-startup-faults", ["/usr/bin/xcrun", "clang", "-dynamiclib", "-Wall", "-Wextra",
|
self.run("build-startup-faults", ["/usr/bin/xcrun", "clang", "-dynamiclib", "-Wall", "-Wextra",
|
||||||
"-O2", source, "-o", library])
|
"-O2", source, "-o", library])
|
||||||
for fault in ("refused", "metadata", "control", "rollback-blocked"):
|
for fault in ("aux-child", "refused", "metadata", "control", "rollback-blocked"):
|
||||||
label = "startup-" + fault
|
label = "startup-" + fault
|
||||||
socket_log = self.root / (label + "-socket.txt")
|
socket_log = self.root / (label + "-socket.txt")
|
||||||
fault_marker = self.root / (label + "-fault-reached")
|
fault_marker = self.root / (label + "-fault-reached")
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ def main():
|
|||||||
vc("mount", "--mount", volume, mountpoint, "--password=" + password,
|
vc("mount", "--mount", volume, mountpoint, "--password=" + password,
|
||||||
"--pim=1", "--keyfiles=", "--protect-hidden=no",
|
"--pim=1", "--keyfiles=", "--protect-hidden=no",
|
||||||
*(["--mount-options=ro"] if read_only else []))
|
*(["--mount-options=ro"] if read_only else []))
|
||||||
identities = list(temporary.glob(".veracrypt_aux_mnt*/shutdown"))
|
identities = list(temporary.glob("**/.veracrypt_aux_mnt*/shutdown"))
|
||||||
assert len(identities) == 1, "Expected one disposable FUSE-T service"
|
assert len(identities) == 1, "Expected one disposable FUSE-T service"
|
||||||
aux = identities[0].parent
|
aux = identities[0].parent
|
||||||
active = (int(identities[0].read_text().split()[0]), aux,
|
active = (int(identities[0].read_text().split()[0]), aux,
|
||||||
@@ -86,7 +86,7 @@ def main():
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise AssertionError("Disposable service still running")
|
raise AssertionError("Disposable service still running")
|
||||||
assert not aux.exists() and not endpoint.exists(), "Cleanup paths remain"
|
assert not aux.exists() and not aux.parent.exists() and not endpoint.exists(), "Cleanup paths remain"
|
||||||
assert str(root) not in mounts(), "Disposable mount remains"
|
assert str(root) not in mounts(), "Disposable mount remains"
|
||||||
handles = subprocess.run(["/usr/sbin/lsof", "-t", str(volume)], capture_output=True, text=True)
|
handles = subprocess.run(["/usr/sbin/lsof", "-t", str(volume)], capture_output=True, text=True)
|
||||||
assert handles.returncode == 1 and not handles.stdout.strip(), "Backing file still open"
|
assert handles.returncode == 1 and not handles.stdout.strip(), "Backing file still open"
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ def main():
|
|||||||
slow = leg.mount(binary, "slow", password, dict(
|
slow = leg.mount(binary, "slow", password, dict(
|
||||||
DYLD_INSERT_LIBRARIES=str(faults), VC_FUSET_TEST_FAULT="cleanup-delay", VC_FUSET_TEST_DELAY="15",
|
DYLD_INSERT_LIBRARIES=str(faults), VC_FUSET_TEST_FAULT="cleanup-delay", VC_FUSET_TEST_DELAY="15",
|
||||||
VC_FUSET_TEST_ROOT=str(root), VC_FUSET_TEST_FAULT_MARKER=str(leg.root / "cleanup-delayed")))
|
VC_FUSET_TEST_ROOT=str(root), VC_FUSET_TEST_FAULT_MARKER=str(leg.root / "cleanup-delayed")))
|
||||||
service = int(next(leg.temporary.glob(".veracrypt_aux_mnt*/shutdown")).read_text().split()[0])
|
service = int(next(leg.temporary.glob("**/.veracrypt_aux_mnt*/shutdown")).read_text().split()[0])
|
||||||
leg.start_gui(bundle)
|
leg.start_gui(bundle)
|
||||||
assert wait_for(lambda: slow not in leg.aux_mounts(), 120), "Automatic unmount did not start"
|
assert wait_for(lambda: slow not in leg.aux_mounts(), 120), "Automatic unmount did not start"
|
||||||
service_alive = lambda: subprocess.run(["/bin/kill", "-0", str(service)], capture_output=True).returncode == 0
|
service_alive = lambda: subprocess.run(["/bin/kill", "-0", str(service)], capture_output=True).returncode == 0
|
||||||
|
|||||||
+100
-1
@@ -18,6 +18,12 @@
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
#if defined(TC_MACOSX) && defined(VC_MACOSX_FUSET)
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <membership.h>
|
||||||
|
#include <sys/acl.h>
|
||||||
|
#include <sys/mount.h>
|
||||||
|
#endif
|
||||||
#ifdef TC_LINUX
|
#ifdef TC_LINUX
|
||||||
#include <sys/utsname.h>
|
#include <sys/utsname.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -32,6 +38,85 @@
|
|||||||
|
|
||||||
namespace VeraCrypt
|
namespace VeraCrypt
|
||||||
{
|
{
|
||||||
|
#if defined(TC_MACOSX) && defined(VC_MACOSX_FUSET)
|
||||||
|
static string CreateFuseTAuxiliaryDirectory (const string &tempDirectory, uid_t userId)
|
||||||
|
{
|
||||||
|
// SMB covers the mountpoint's permissions and does not preserve the
|
||||||
|
// requesting local uid. Enforce access on an unmounted parent instead,
|
||||||
|
// including when sudo removes TMPDIR or a user selects a shared TMPDIR.
|
||||||
|
const bool elevated = geteuid() == 0;
|
||||||
|
const bool userAcl = elevated && userId != 0;
|
||||||
|
const string directoryTemplate = tempDirectory + (elevated ? "/.veracrypt_aux_root_" : "/.veracrypt_aux_")
|
||||||
|
+ StringConverter::ToSingle (static_cast <uint64> (userId)) + "-XXXXXXXXXXXX";
|
||||||
|
uuid_t userUuid;
|
||||||
|
if (userAcl && mbr_uid_to_uuid (userId, userUuid) != 0)
|
||||||
|
throw TemporaryDirectoryFailure (SRC_POS, StringConverter::ToWide (directoryTemplate));
|
||||||
|
vector <char> temporary (directoryTemplate.begin(), directoryTemplate.end());
|
||||||
|
temporary.push_back ('\0');
|
||||||
|
throw_sys_sub_if (mkdtemp (&temporary[0]) == NULL, tempDirectory);
|
||||||
|
bool ready = false;
|
||||||
|
finally_do_arg2 (const char *, &temporary[0], bool &, ready, { if (!finally_arg2) rmdir (finally_arg); });
|
||||||
|
const string directory = &temporary[0];
|
||||||
|
|
||||||
|
const int fd = open (directory.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||||
|
throw_sys_sub_if (fd == -1, directory);
|
||||||
|
finally_do_arg (int, fd, { close (finally_arg); });
|
||||||
|
struct statfs filesystem;
|
||||||
|
throw_sys_sub_if (fstatfs (fd, &filesystem) == -1, directory);
|
||||||
|
if (filesystem.f_flags & MNT_IGNORE_OWNERSHIP)
|
||||||
|
throw TemporaryDirectoryFailure (SRC_POS, StringConverter::ToWide (directory)
|
||||||
|
+ L"\nThe temporary filesystem must enforce ownership.");
|
||||||
|
struct stat info;
|
||||||
|
throw_sys_sub_if (fstat (fd, &info) == -1, directory);
|
||||||
|
if (!S_ISDIR (info.st_mode) || info.st_uid != (elevated ? 0 : userId))
|
||||||
|
throw TemporaryDirectoryFailure (SRC_POS, StringConverter::ToWide (directory));
|
||||||
|
// Keep elevated parents root-owned so the caller cannot replace paths
|
||||||
|
// used by privileged setup. Clear inherited ACLs even without a grant.
|
||||||
|
acl_t acl = acl_init (userAcl ? 1 : 0);
|
||||||
|
throw_sys_sub_if (acl == NULL, directory);
|
||||||
|
finally_do_arg (acl_t *, &acl, { acl_free (*finally_arg); });
|
||||||
|
if (userAcl)
|
||||||
|
{
|
||||||
|
acl_entry_t entry;
|
||||||
|
throw_sys_sub_if (acl_create_entry (&acl, &entry) == -1, directory);
|
||||||
|
throw_sys_sub_if (acl_set_tag_type (entry, ACL_EXTENDED_ALLOW) == -1, directory);
|
||||||
|
throw_sys_sub_if (acl_set_qualifier (entry, userUuid) == -1, directory);
|
||||||
|
throw_sys_sub_if (acl_set_permset_mask_np (entry,
|
||||||
|
ACL_LIST_DIRECTORY | ACL_SEARCH | ACL_READ_ATTRIBUTES | ACL_READ_SECURITY) == -1, directory);
|
||||||
|
}
|
||||||
|
throw_sys_sub_if (acl_set_fd_np (fd, acl, ACL_TYPE_EXTENDED) == -1, directory);
|
||||||
|
throw_sys_sub_if (fchmod (fd, 0700) == -1, directory);
|
||||||
|
throw_sys_sub_if (fstat (fd, &info) == -1, directory);
|
||||||
|
if ((info.st_mode & 0777) != 0700)
|
||||||
|
throw TemporaryDirectoryFailure (SRC_POS, StringConverter::ToWide (directory));
|
||||||
|
|
||||||
|
ready = true;
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsOtherUsersFuseTAuxiliaryMount (const MountedFilesystem &mount, uid_t userId, uid_t realUserId)
|
||||||
|
{
|
||||||
|
// Preserve root's existing discovery scope; only unprivileged callers
|
||||||
|
// can be excluded by the private parent.
|
||||||
|
if (mount.Type != "smbfs" || mount.Owner != 0 || userId == 0)
|
||||||
|
return false;
|
||||||
|
const string path = mount.MountPoint;
|
||||||
|
const string parent = path.substr (0, path.find_last_of ('/'));
|
||||||
|
const string name = parent.substr (parent.find_last_of ('/') + 1);
|
||||||
|
const string prefix = ".veracrypt_aux_root_";
|
||||||
|
if (name.compare (0, prefix.size(), prefix) == 0)
|
||||||
|
{
|
||||||
|
const string id = name.substr (prefix.size(), name.find ('-', prefix.size()) - prefix.size());
|
||||||
|
if (!id.empty() && id.find_first_not_of ("0123456789") == string::npos)
|
||||||
|
return id != StringConverter::ToSingle (static_cast <uint64> (userId))
|
||||||
|
&& id != StringConverter::ToSingle (static_cast <uint64> (realUserId));
|
||||||
|
}
|
||||||
|
// Preserve metadata discovery for legacy paths: an ACL may grant
|
||||||
|
// traversal despite a foreign-owned parent with mode 0700.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef TC_LINUX
|
#ifdef TC_LINUX
|
||||||
static string GetTmpUser ();
|
static string GetTmpUser ();
|
||||||
static bool GetLinuxKernelVersion (int &kernelMajor, int &kernelMinor);
|
static bool GetLinuxKernelVersion (int &kernelMajor, int &kernelMinor);
|
||||||
@@ -563,6 +648,14 @@ namespace VeraCrypt
|
|||||||
#endif
|
#endif
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
#if defined(TC_MACOSX) && defined(VC_MACOSX_FUSET)
|
||||||
|
// Elevated SMB mounts have a root mount-table owner. Their private
|
||||||
|
// parent identifies the user; an inaccessible foreign mount is not
|
||||||
|
// an unresolved mount of the current user.
|
||||||
|
if (IsOtherUsersFuseTAuxiliaryMount (mf, getuid(), GetRealUserId()))
|
||||||
|
continue;
|
||||||
|
#endif
|
||||||
|
|
||||||
shared_ptr <VolumeInfo> mountedVol;
|
shared_ptr <VolumeInfo> mountedVol;
|
||||||
// Introduce a retry mechanism with a timeout for control file access.
|
// Introduce a retry mechanism with a timeout for control file access.
|
||||||
// The list is already filtered to VeraCrypt auxiliary mounts; in
|
// The list is already filtered to VeraCrypt auxiliary mounts; in
|
||||||
@@ -1174,7 +1267,13 @@ namespace VeraCrypt
|
|||||||
// An older service may still be shutting down after its SMB mount has
|
// An older service may still be shutting down after its SMB mount has
|
||||||
// disappeared. FUSE-T also uses the pathname during backend teardown,
|
// disappeared. FUSE-T also uses the pathname during backend teardown,
|
||||||
// so a replacement volume must have a different auxiliary path.
|
// so a replacement volume must have a different auxiliary path.
|
||||||
string mountTemplate = string (GetTempDirectory()) + "/" + GetFuseMountDirPrefix() + "-XXXXXXXXXXXX";
|
// An elevated parent also needs trusted ancestors. Match the shutdown
|
||||||
|
// socket's location instead of honoring a caller-controlled TMPDIR.
|
||||||
|
const string auxiliaryParent = CreateFuseTAuxiliaryDirectory (geteuid() == 0 ? "/private/tmp" : GetTempDirectory(), GetRealUserId());
|
||||||
|
// Cover failures before the service takes over, including child creation.
|
||||||
|
// A live mount keeps this parent nonempty; its service removes it later.
|
||||||
|
finally_do_arg (string, auxiliaryParent, { rmdir (finally_arg.c_str()); });
|
||||||
|
string mountTemplate = auxiliaryParent + "/" + GetFuseMountDirPrefix() + "-XXXXXXXXXXXX";
|
||||||
vector <char> mountDirectory (mountTemplate.begin(), mountTemplate.end());
|
vector <char> mountDirectory (mountTemplate.begin(), mountTemplate.end());
|
||||||
mountDirectory.push_back ('\0');
|
mountDirectory.push_back ('\0');
|
||||||
throw_sys_if (mkdtemp (&mountDirectory[0]) == NULL);
|
throw_sys_if (mkdtemp (&mountDirectory[0]) == NULL);
|
||||||
|
|||||||
@@ -415,7 +415,12 @@ namespace VeraCrypt
|
|||||||
// Current services remove their original directory. Older development
|
// Current services remove their original directory. Older development
|
||||||
// services may leave it behind; rmdir only removes an empty directory
|
// services may leave it behind; rmdir only removes an empty directory
|
||||||
// and cannot follow a replacement symlink or remove a mounted filesystem.
|
// and cannot follow a replacement symlink or remove a mounted filesystem.
|
||||||
rmdir (string (mountedVolume->AuxMountPoint).c_str());
|
if (rmdir (string (mountedVolume->AuxMountPoint).c_str()) == 0)
|
||||||
|
{
|
||||||
|
#ifdef VC_MACOSX_FUSET
|
||||||
|
FuseService::RemoveAuxMountParent (mountedVolume->AuxMountPoint);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
return mountedVolume;
|
return mountedVolume;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -545,9 +545,11 @@ namespace VeraCrypt
|
|||||||
if (!fuse_service_find_mount (Path.c_str(), mountId))
|
if (!fuse_service_find_mount (Path.c_str(), mountId))
|
||||||
{
|
{
|
||||||
struct stat current;
|
struct stat current;
|
||||||
if (fstatat (ParentFd, Name.c_str(), ¤t, AT_SYMLINK_NOFOLLOW) == 0
|
int status = fstatat (ParentFd, Name.c_str(), ¤t, AT_SYMLINK_NOFOLLOW);
|
||||||
&& S_ISDIR (current.st_mode) && current.st_dev == Original.st_dev && current.st_ino == Original.st_ino)
|
if ((status == -1 && errno == ENOENT)
|
||||||
unlinkat (ParentFd, Name.c_str(), AT_REMOVEDIR);
|
|| (status == 0 && S_ISDIR (current.st_mode) && current.st_dev == Original.st_dev && current.st_ino == Original.st_ino
|
||||||
|
&& unlinkat (ParentFd, Name.c_str(), AT_REMOVEDIR) == 0))
|
||||||
|
FuseService::RemoveAuxMountParent (Path, ParentFd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (...) { }
|
catch (...) { }
|
||||||
@@ -561,6 +563,28 @@ namespace VeraCrypt
|
|||||||
struct stat Original;
|
struct stat Original;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void FuseService::RemoveAuxMountParent (const string &fuseMountPoint, int parentFd)
|
||||||
|
{
|
||||||
|
const string parent = fuseMountPoint.substr (0, fuseMountPoint.find_last_of ('/'));
|
||||||
|
const string name = parent.substr (parent.find_last_of ('/') + 1);
|
||||||
|
const string prefix = name.find (".veracrypt_aux_root_") == 0 ? ".veracrypt_aux_root_" : ".veracrypt_aux_";
|
||||||
|
const size_t separator = name.find ('-', prefix.size());
|
||||||
|
// Only the per-mount format belongs to us. Legacy parents may be a
|
||||||
|
// shared per-user directory or an arbitrary caller-selected TMPDIR.
|
||||||
|
if (name.compare (0, prefix.size(), prefix) != 0 || separator == string::npos
|
||||||
|
|| separator == prefix.size() || name.size() - separator - 1 != 12
|
||||||
|
|| name.substr (prefix.size(), separator - prefix.size()).find_first_not_of ("0123456789") != string::npos
|
||||||
|
|| name.substr (separator + 1).find_first_not_of ("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") != string::npos)
|
||||||
|
return;
|
||||||
|
struct stat current, original;
|
||||||
|
if (lstat (parent.c_str(), ¤t) != 0 || !S_ISDIR (current.st_mode) || current.st_uid != geteuid())
|
||||||
|
return;
|
||||||
|
if (parentFd != -1 && (fstat (parentFd, &original) != 0
|
||||||
|
|| current.st_dev != original.st_dev || current.st_ino != original.st_ino))
|
||||||
|
return;
|
||||||
|
rmdir (parent.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
static int fuse_service_main (int argc, char *argv[], const struct fuse_operations *operations, int startupFd)
|
static int fuse_service_main (int argc, char *argv[], const struct fuse_operations *operations, int startupFd)
|
||||||
{
|
{
|
||||||
// On rollback, EOF must only reach the caller after fuse_destroy has
|
// On rollback, EOF must only reach the caller after fuse_destroy has
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ namespace VeraCrypt
|
|||||||
#if defined(TC_MACOSX) && defined(VC_MACOSX_FUSET)
|
#if defined(TC_MACOSX) && defined(VC_MACOSX_FUSET)
|
||||||
static const char *GetShutdownPath () { return "/shutdown"; }
|
static const char *GetShutdownPath () { return "/shutdown"; }
|
||||||
static const char *GetShutdownSocketPath () { return "/shutdown-socket"; }
|
static const char *GetShutdownSocketPath () { return "/shutdown-socket"; }
|
||||||
|
static void RemoveAuxMountParent (const string &fuseMountPoint, int parentFd = -1);
|
||||||
#endif
|
#endif
|
||||||
static string GetDeviceType () { return "veracrypt"; }
|
static string GetDeviceType () { return "veracrypt"; }
|
||||||
static gid_t GetGroupId () { return GroupId; }
|
static gid_t GetGroupId () { return GroupId; }
|
||||||
|
|||||||
Reference in new issue
Block a user