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

+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; }