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:
Mounir IDRASSI committed 2026-09-29 10:01:37 +02:00
1 parent c91f386f00
commit 41bc8e5f6a
11 files changed
+442 -12

No files matched your search

+17
View File
@@ -14,8 +14,25 @@ build, then run:
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_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
with an in-memory inventory provider. It checks exact and legacy alias matches,
unresolved inventory, stale-field invalidation, one query per refresh, and fresh
+12
View File
@@ -93,6 +93,17 @@ static int test_stat (const char *path, struct stat *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)
{
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_open, open);
INTERPOSE (test_stat, stat);
INTERPOSE (test_mkdtemp, mkdtemp);
INTERPOSE (test_unmount, unmount);
INTERPOSE (test_unlinkat, unlinkat);
+265
View File
@@ -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()
+1
View File
@@ -263,6 +263,7 @@ struct FuseService {
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 RemoveAuxMountParent(const std::string &) { throw std::runtime_error("unexpected directory cleanup"); }
};
struct Process {
static std::string Execute(const std::string &,const std::list<std::string> &) { ++Commands; throw std::runtime_error("unexpected disk command"); }
+10 -4
View File
@@ -81,11 +81,16 @@ class DismountChecks:
self.vc(label, "--mount", self.volume, self.mountpoint,
"--password=" + self.password, "--pim=1", "--keyfiles=",
"--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:
raise AssertionError(f"Expected one test service, found {auxiliaries}")
aux = auxiliaries[0]
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"
if identity.exists():
pid, serial, slot = map(int, identity.read_text().split())
@@ -173,8 +178,9 @@ class DismountChecks:
endpoint = self.active.get("endpoint")
state = dict(label=label, mounts=self.mounted_paths(), handles=handles.stdout,
service_alive=alive, endpoint_exists=bool(endpoint and endpoint.exists()),
backend=backend, services=services, images=images)
if not any(state[key] for key in ("mounts", "handles", "service_alive", "endpoint_exists", "backend", "services", "images")):
backend=backend, services=services, images=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.active = None
return
@@ -320,7 +326,7 @@ class DismountChecks:
source = Path(__file__).with_name("fuset_startup_faults.c")
self.run("build-startup-faults", ["/usr/bin/xcrun", "clang", "-dynamiclib", "-Wall", "-Wextra",
"-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
socket_log = self.root / (label + "-socket.txt")
fault_marker = self.root / (label + "-fault-reached")
+2 -2
View File
@@ -71,7 +71,7 @@ def main():
vc("mount", "--mount", volume, mountpoint, "--password=" + password,
"--pim=1", "--keyfiles=", "--protect-hidden=no",
*(["--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"
aux = identities[0].parent
active = (int(identities[0].read_text().split()[0]), aux,
@@ -86,7 +86,7 @@ def main():
pass
else:
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"
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"
+1 -1
View File
@@ -167,7 +167,7 @@ def main():
slow = leg.mount(binary, "slow", password, dict(
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")))
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)
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
+100 -1
View File
@@ -18,6 +18,12 @@
#include <sys/stat.h>
#include <sys/time.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
#include <sys/utsname.h>
#endif
@@ -32,6 +38,85 @@
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
static string GetTmpUser ();
static bool GetLinuxKernelVersion (int &kernelMajor, int &kernelMinor);
@@ -563,6 +648,14 @@ namespace VeraCrypt
#endif
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;
// Introduce a retry mechanism with a timeout for control file access.
// 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
// disappeared. FUSE-T also uses the pathname during backend teardown,
// so a replacement volume must have a different auxiliary path.
string mountTemplate = string (GetTempDirectory()) + "/" + GetFuseMountDirPrefix() + "-XXXXXXXXXXXX";
// 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());
mountDirectory.push_back ('\0');
throw_sys_if (mkdtemp (&mountDirectory[0]) == NULL);
+6 -1
View File
@@ -415,7 +415,12 @@ namespace VeraCrypt
// Current services remove their original directory. Older development
// services may leave it behind; rmdir only removes an empty directory
// 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;
}
+27 -3
View File
@@ -545,9 +545,11 @@ namespace VeraCrypt
if (!fuse_service_find_mount (Path.c_str(), mountId))
{
struct stat current;
if (fstatat (ParentFd, Name.c_str(), &current, AT_SYMLINK_NOFOLLOW) == 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);
int status = fstatat (ParentFd, Name.c_str(), &current, AT_SYMLINK_NOFOLLOW);
if ((status == -1 && errno == ENOENT)
|| (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 (...) { }
@@ -561,6 +563,28 @@ namespace VeraCrypt
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(), &current) != 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)
{
// On rollback, EOF must only reach the caller after fuse_destroy has
+1
View File
@@ -61,6 +61,7 @@ namespace VeraCrypt
#if defined(TC_MACOSX) && defined(VC_MACOSX_FUSET)
static const char *GetShutdownPath () { return "/shutdown"; }
static const char *GetShutdownSocketPath () { return "/shutdown-socket"; }
static void RemoveAuxMountParent (const string &fuseMountPoint, int parentFd = -1);
#endif
static string GetDeviceType () { return "veracrypt"; }
static gid_t GetGroupId () { return GroupId; }