diff --git a/.gitignore b/.gitignore
index 11201ffb81..b6ec782f74 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,6 +36,10 @@ src/Main/VeraCrypt
src/Main/VeraCrypt.app
src/Main/*.dmg
src/Setup/MacOSX/*.pkg
+# macOS privileged helper build artifacts (binary + plists generated from templates)
+src/PrivilegedHelper/org.idrix.VeraCrypt.helper
+src/PrivilegedHelper/Helper-Info.plist
+src/PrivilegedHelper/Helper-Launchd.plist
*.oo
*.o.32
*.o.64
diff --git a/src/Build/Resources/MacOSX/Helper-Info.plist.xml b/src/Build/Resources/MacOSX/Helper-Info.plist.xml
new file mode 100644
index 0000000000..bd5d1e4b61
--- /dev/null
+++ b/src/Build/Resources/MacOSX/Helper-Info.plist.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ CFBundleInfoDictionaryVersion
+ 6.0
+
+ CFBundleIdentifier
+ org.idrix.VeraCrypt.helper
+
+ CFBundleName
+ VeraCrypt Privileged Helper
+
+ CFBundlePackageType
+ APPL
+
+ CFBundleVersion
+ _VERSION_
+
+ CFBundleShortVersionString
+ _VERSION_
+
+
+ SMAuthorizedClients
+
+ identifier "org.idrix.VeraCrypt" and anchor apple generic and certificate leaf[subject.OU] = "_TEAMID_"
+
+
+
diff --git a/src/Build/Resources/MacOSX/Helper-Launchd.plist.xml b/src/Build/Resources/MacOSX/Helper-Launchd.plist.xml
new file mode 100644
index 0000000000..df446d9b76
--- /dev/null
+++ b/src/Build/Resources/MacOSX/Helper-Launchd.plist.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Label
+ org.idrix.VeraCrypt.helper
+
+
+ MachServices
+
+ org.idrix.VeraCrypt.helper
+
+
+
+
diff --git a/src/Build/Resources/MacOSX/Info.plist.xml b/src/Build/Resources/MacOSX/Info.plist.xml
index 04ed21c1c2..ad48098e8c 100644
--- a/src/Build/Resources/MacOSX/Info.plist.xml
+++ b/src/Build/Resources/MacOSX/Info.plist.xml
@@ -96,5 +96,15 @@
NSPrincipalClass
NSApplication
+
+
+ SMPrivilegedExecutables
+
+ org.idrix.VeraCrypt.helper
+ identifier "org.idrix.VeraCrypt.helper" and anchor apple generic and certificate leaf[subject.OU] = "_TEAMID_"
+
diff --git a/src/Core/Core.make b/src/Core/Core.make
index def349d812..3dd2065471 100644
--- a/src/Core/Core.make
+++ b/src/Core/Core.make
@@ -26,6 +26,7 @@ OBJS += Unix/$(PLATFORM)/Core$(PLATFORM).o
OBJS += Unix/$(PLATFORM)/Core$(PLATFORM).o
ifeq "$(PLATFORM)" "MacOSX"
OBJS += Unix/FreeBSD/CoreFreeBSD.o
+OBJS += Unix/MacOSX/PrivilegedHelperClient.o
endif
include $(BUILD_INC)/Makefile.inc
diff --git a/src/Core/Unix/CoreService.cpp b/src/Core/Unix/CoreService.cpp
index 014b5c8f0f..1f6a33af75 100644
--- a/src/Core/Unix/CoreService.cpp
+++ b/src/Core/Unix/CoreService.cpp
@@ -15,6 +15,9 @@
#include
#include
#include
+#ifdef TC_MACOSX
+#include "Core/Unix/MacOSX/PrivilegedHelperClient.h"
+#endif
#include "Platform/FileStream.h"
#include "Platform/MemoryStream.h"
#include "Platform/Serializable.h"
@@ -61,13 +64,12 @@ namespace VeraCrypt
|| IsMacOSXDevicePathWithPrefix (path, "/dev/rdisk");
}
- // The elevated service runs as root, so it must not be tricked into changing
- // ownership of an arbitrary path. Every legitimate macOS caller of the
- // elevated SetFileOwner targets a real disk device node (/dev/[r]diskN[sM]),
- // so restrict the operation to that. lstat() (not stat) is used so a symlink
- // is rejected outright rather than followed, and the st_mode check confirms an
- // actual block/character device before the chown.
- static void ValidateMacOSXSetFileOwnerTarget (const FilesystemPath &path)
+ // The elevated service runs as root, so it must not be tricked into operating
+ // on arbitrary paths. Every legitimate macOS caller that reaches these helpers
+ // targets a real disk device node (/dev/[r]diskN[sM]), so restrict privileged
+ // operations to that. lstat() (not stat) rejects symlinks outright, and the
+ // st_mode check confirms an actual block/character device before use.
+ static void ValidateMacOSXDeviceNodeTarget (const FilesystemPath &path)
{
const string pathStr = path;
@@ -84,8 +86,7 @@ namespace VeraCrypt
static list BuildMacOSXAPFSFormatterArguments (const ExecuteMacOSXAPFSFormatterRequest &request)
{
- if (!IsMacOSXFormatterDevicePath (request.Device))
- throw ParameterIncorrect (SRC_POS);
+ ValidateMacOSXDeviceNodeTarget (request.Device);
if (request.OwnerUserId > static_cast ((uid_t) -1)
|| request.OwnerGroupId > static_cast ((gid_t) -1))
@@ -330,7 +331,7 @@ namespace VeraCrypt
#ifdef TC_MACOSX
// Restrict the root-privileged chown to real disk device nodes.
- ValidateMacOSXSetFileOwnerTarget (setFileOwnerRequest->Path);
+ ValidateMacOSXDeviceNodeTarget (setFileOwnerRequest->Path);
#endif
coreUnix->SetFileOwner (setFileOwnerRequest->Path, setFileOwnerRequest->Owner);
SetFileOwnerResponse().Serialize (outputStream);
@@ -449,6 +450,21 @@ namespace VeraCrypt
{
// Test if the user has an active "sudo" session.
bool authCheckDone = false;
+#ifdef TC_MACOSX
+ // macOS: establish the privileged channel here, in the main
+ // application process. StartElevated() uses the SMJobBless helper
+ // and XPC, which cannot run in the unprivileged core service (a
+ // fork()ed child that never calls exec()); delegating elevation to
+ // it fails before the native authentication dialog is even shown.
+ // StartElevated() repoints the Service streams at the root core
+ // service, so the request is then sent below like any other.
+ authCheckDone = true;
+ request.FastElevation = false;
+ StartElevated (request);
+ ElevatedServiceAvailable = true;
+ request.Serialize (ServiceInputStream);
+ return GetResponse ();
+#else
if (!Core->GetUseDummySudoPassword ())
{
// We are using -n to avoid prompting the user for a password.
@@ -493,6 +509,7 @@ namespace VeraCrypt
request.FastElevation = false;
}
}
+#endif
try
{
@@ -527,6 +544,9 @@ namespace VeraCrypt
request.FastElevation = false;
+#ifdef TC_MACOSX
+ throw;
+#endif
if(!authCheckDone)
(*AdminPasswordCallback) (request.AdminPassword);
}
@@ -562,6 +582,51 @@ namespace VeraCrypt
void CoreService::StartElevated (const CoreServiceRequest &request)
{
+#ifdef TC_MACOSX
+ try
+ {
+ std::string errorMsg;
+ string appPath = request.ApplicationExecutablePath;
+ if (appPath.empty() || appPath[0] != '/')
+ {
+ appPath = Process::FindSystemBinary("VeraCrypt", errorMsg);
+ if (appPath.empty())
+ throw SystemException(SRC_POS, errorMsg);
+ }
+
+ // Install (if needed) and drive the SMJobBless privileged helper.
+ // The helper shows the native macOS authentication dialog at install
+ // time, validates this app's code signature on every connection, and
+ // spawns " --core-service" as root, returning a connected
+ // socket. VeraCrypt never handles the administrator password.
+ int serviceFD = MacOSXConnectElevatedCoreService (appPath);
+ throw_sys_if (serviceFD == -1);
+
+ shared_ptr servicePipe (new File());
+ servicePipe->AssignSystemHandle (serviceFD, false);
+ ServiceInputStream = shared_ptr (new FileStream (servicePipe));
+ ServiceOutputStream = shared_ptr (new FileStream (servicePipe));
+
+ // Send sync code
+ uint8 sync[] = { 0, 0x11, 0x22 };
+ ServiceInputStream->Write (ConstBufferPtr (sync, array_capacity (sync)));
+
+ return;
+ }
+ catch (Exception &)
+ {
+ throw;
+ }
+ catch (exception &e)
+ {
+ throw ExternalException (SRC_POS, StringConverter::ToExceptionString (e));
+ }
+ catch (...)
+ {
+ throw UnknownException (SRC_POS);
+ }
+#endif
+
unique_ptr inPipe (new Pipe());
unique_ptr outPipe (new Pipe());
Pipe errPipe;
diff --git a/src/Core/Unix/MacOSX/PrivilegedHelperClient.h b/src/Core/Unix/MacOSX/PrivilegedHelperClient.h
new file mode 100644
index 0000000000..79548b1bb7
--- /dev/null
+++ b/src/Core/Unix/MacOSX/PrivilegedHelperClient.h
@@ -0,0 +1,24 @@
+/*
+ Copyright (c) 2026 AM Crypto and are governed by the Apache License 2.0
+ the full text of which is contained in the file License.txt included in
+ VeraCrypt binary and source code distribution packages.
+*/
+
+#ifndef TC_HEADER_Core_Unix_MacOSX_PrivilegedHelperClient
+#define TC_HEADER_Core_Unix_MacOSX_PrivilegedHelperClient
+
+#include
+
+namespace VeraCrypt
+{
+ // Ensures the SMJobBless privileged helper is installed and up to date
+ // (showing the native macOS authentication dialog when an install/upgrade
+ // is required), then asks it to spawn "appPath --core-service" as root and
+ // returns a connected socket file descriptor to that root process. The
+ // returned descriptor is owned by the caller and must be closed.
+ //
+ // Throws a VeraCrypt exception (e.g. ElevationFailed) on any failure.
+ int MacOSXConnectElevatedCoreService (const std::string &appPath);
+}
+
+#endif // TC_HEADER_Core_Unix_MacOSX_PrivilegedHelperClient
diff --git a/src/Core/Unix/MacOSX/PrivilegedHelperClient.mm b/src/Core/Unix/MacOSX/PrivilegedHelperClient.mm
new file mode 100644
index 0000000000..6c1dde1230
--- /dev/null
+++ b/src/Core/Unix/MacOSX/PrivilegedHelperClient.mm
@@ -0,0 +1,160 @@
+/*
+ Copyright (c) 2026 AM Crypto and are governed by the Apache License 2.0
+ the full text of which is contained in the file License.txt included in
+ VeraCrypt binary and source code distribution packages.
+*/
+
+// Client-side glue for the VeraCrypt SMJobBless privileged helper.
+//
+// This translation unit is compiled as Objective-C++ (".mm") so that it can use
+// the Blocks-based XPC and ServiceManagement APIs (the rest of CoreService.cpp
+// is plain C++ and is not compiled with -fblocks). It exposes a single plain
+// C++ entry point, MacOSXConnectElevatedCoreService(), declared in
+// PrivilegedHelperClient.h and called from CoreService::StartElevated().
+
+#include "PrivilegedHelperClient.h"
+#include "PrivilegedHelperProtocol.h"
+
+// The Apple framework headers must come first: they typedef BOOL (objc.h),
+// which must be seen before Common/Tcdefs.h redefines BOOL as a macro. They
+// also pull in , whose ERR_SUCCESS macro would otherwise mangle
+// the ERR_SUCCESS enumerator in Tcdefs.h, so it is undefined before the
+// VeraCrypt headers (reached via SystemException.h) are included.
+#include
+#include
+#include
+#include
+
+#undef ERR_SUCCESS
+
+#include "Platform/SystemException.h"
+#include "Core/CoreException.h"
+
+namespace VeraCrypt
+{
+ // Connects to the helper's privileged Mach service. The connection is
+ // returned resumed; the caller owns it and must xpc_connection_cancel() it.
+ static xpc_connection_t ConnectToHelper ()
+ {
+ xpc_connection_t connection = xpc_connection_create_mach_service (
+ VC_HELPER_LABEL, NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED);
+
+ if (!connection)
+ throw ElevationFailed (SRC_POS, VC_HELPER_LABEL, 1, "xpc_connection_create_mach_service failed");
+
+ // A no-op event handler is mandatory; per-request errors are surfaced
+ // synchronously through the reply objects below.
+ xpc_connection_set_event_handler (connection, ^(xpc_object_t) { });
+ xpc_connection_resume (connection);
+ return connection;
+ }
+
+ // Returns the version reported by the currently installed helper, or -1 if
+ // no helper is installed / reachable.
+ static int64_t QueryHelperVersion ()
+ {
+ xpc_connection_t connection = ConnectToHelper ();
+
+ xpc_object_t message = xpc_dictionary_create (NULL, NULL, 0);
+ xpc_dictionary_set_string (message, VC_HELPER_KEY_COMMAND, VC_HELPER_CMD_GET_VERSION);
+
+ xpc_object_t reply = xpc_connection_send_message_with_reply_sync (connection, message);
+
+ int64_t version = -1;
+ if (xpc_get_type (reply) == XPC_TYPE_DICTIONARY)
+ version = xpc_dictionary_get_int64 (reply, VC_HELPER_KEY_VERSION);
+
+ xpc_connection_cancel (connection);
+ return version;
+ }
+
+ // Installs (or upgrades) the helper via SMJobBless. This is the call that
+ // triggers the native macOS authentication dialog.
+ static void BlessHelper ()
+ {
+ AuthorizationRef authRef = NULL;
+ OSStatus status = AuthorizationCreate (NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &authRef);
+ if (status != errAuthorizationSuccess)
+ throw ElevationFailed (SRC_POS, "AuthorizationCreate", status, "");
+
+ AuthorizationItem item = { kSMRightBlessPrivilegedHelper, 0, NULL, 0 };
+ AuthorizationRights rights = { 1, &item };
+ AuthorizationFlags flags = kAuthorizationFlagDefaults
+ | kAuthorizationFlagInteractionAllowed
+ | kAuthorizationFlagPreAuthorize
+ | kAuthorizationFlagExtendRights;
+
+ status = AuthorizationCopyRights (authRef, &rights, kAuthorizationEmptyEnvironment, flags, NULL);
+ if (status != errAuthorizationSuccess)
+ {
+ AuthorizationFree (authRef, kAuthorizationFlagDefaults);
+ // errAuthorizationCanceled when the user dismisses the dialog.
+ throw ElevationFailed (SRC_POS, "AuthorizationCopyRights", status, "");
+ }
+
+ CFErrorRef cfError = NULL;
+ Boolean blessed = SMJobBless (kSMDomainSystemLaunchd, CFSTR (VC_HELPER_LABEL), authRef, &cfError);
+ AuthorizationFree (authRef, kAuthorizationFlagDefaults);
+
+ if (!blessed)
+ {
+ long code = cfError ? (long) CFErrorGetCode (cfError) : 0;
+ string description = "SMJobBless failed";
+ if (cfError)
+ {
+ CFStringRef desc = CFErrorCopyDescription (cfError);
+ if (desc)
+ {
+ char buffer[512];
+ if (CFStringGetCString (desc, buffer, sizeof (buffer), kCFStringEncodingUTF8))
+ description = buffer;
+ CFRelease (desc);
+ }
+ CFRelease (cfError);
+ }
+ throw ElevationFailed (SRC_POS, "SMJobBless", (int) code, description);
+ }
+ }
+
+ // Installs the helper if it is missing or its version does not match the
+ // version this build expects.
+ static void EnsureHelperInstalled ()
+ {
+ if (QueryHelperVersion () == VC_HELPER_VERSION)
+ return;
+
+ BlessHelper ();
+ }
+
+ int MacOSXConnectElevatedCoreService (const std::string &appPath)
+ {
+ EnsureHelperInstalled ();
+
+ xpc_connection_t connection = ConnectToHelper ();
+
+ xpc_object_t message = xpc_dictionary_create (NULL, NULL, 0);
+ xpc_dictionary_set_string (message, VC_HELPER_KEY_COMMAND, VC_HELPER_CMD_OPEN_CORE_SERVICE);
+ xpc_dictionary_set_string (message, VC_HELPER_KEY_APP_PATH, appPath.c_str());
+ xpc_dictionary_set_int64 (message, VC_HELPER_KEY_VERSION, VC_HELPER_VERSION);
+
+ xpc_object_t reply = xpc_connection_send_message_with_reply_sync (connection, message);
+
+ if (xpc_get_type (reply) != XPC_TYPE_DICTIONARY)
+ {
+ xpc_connection_cancel (connection);
+ throw ElevationFailed (SRC_POS, VC_HELPER_LABEL, 1, "Privileged helper did not return a valid reply");
+ }
+
+ int serviceFD = xpc_dictionary_dup_fd (reply, VC_HELPER_KEY_SERVICE_FD);
+ if (serviceFD < 0)
+ {
+ const char *helperError = xpc_dictionary_get_string (reply, VC_HELPER_KEY_ERROR);
+ string description = helperError ? helperError : "Privileged helper refused to open the core service";
+ xpc_connection_cancel (connection);
+ throw ElevationFailed (SRC_POS, VC_HELPER_LABEL, 1, description);
+ }
+
+ xpc_connection_cancel (connection);
+ return serviceFD;
+ }
+}
diff --git a/src/Core/Unix/MacOSX/PrivilegedHelperProtocol.h b/src/Core/Unix/MacOSX/PrivilegedHelperProtocol.h
new file mode 100644
index 0000000000..ed7edbd130
--- /dev/null
+++ b/src/Core/Unix/MacOSX/PrivilegedHelperProtocol.h
@@ -0,0 +1,39 @@
+/*
+ Copyright (c) 2026 AM Crypto and are governed by the Apache License 2.0
+ the full text of which is contained in the file License.txt included in
+ VeraCrypt binary and source code distribution packages.
+*/
+
+#ifndef TC_HEADER_Core_Unix_MacOSX_PrivilegedHelperProtocol
+#define TC_HEADER_Core_Unix_MacOSX_PrivilegedHelperProtocol
+
+// Shared constants for the VeraCrypt privileged helper (SMJobBless / launchd).
+// Included by both the helper (PrivilegedHelper/Helper.cpp) and the client glue
+// (Core/Unix/MacOSX/PrivilegedHelperClient.mm).
+
+// launchd Label and Mach service name. Must match Helper-Launchd.plist.xml,
+// Helper-Info.plist.xml (CFBundleIdentifier) and the SMPrivilegedExecutables
+// key of the application's Info.plist.
+#define VC_HELPER_LABEL "org.idrix.VeraCrypt.helper"
+
+// Absolute path where SMJobBless installs the helper tool and its launchd job.
+#define VC_HELPER_TOOL_PATH "/Library/PrivilegedHelperTools/" VC_HELPER_LABEL
+#define VC_HELPER_PLIST_PATH "/Library/LaunchDaemons/" VC_HELPER_LABEL ".plist"
+
+// XPC message keys.
+#define VC_HELPER_KEY_COMMAND "command"
+#define VC_HELPER_KEY_APP_PATH "app-path"
+#define VC_HELPER_KEY_VERSION "version"
+#define VC_HELPER_KEY_SERVICE_FD "service-fd"
+#define VC_HELPER_KEY_ERROR "error"
+
+// XPC commands (value of VC_HELPER_KEY_COMMAND).
+#define VC_HELPER_CMD_OPEN_CORE_SERVICE "open-core-service"
+#define VC_HELPER_CMD_GET_VERSION "get-version"
+#define VC_HELPER_CMD_UNINSTALL "uninstall"
+
+// Protocol/helper version. Bump whenever Helper.cpp changes so that an outdated
+// installed helper is detected by the client and re-blessed (standard pattern).
+#define VC_HELPER_VERSION 1
+
+#endif // TC_HEADER_Core_Unix_MacOSX_PrivilegedHelperProtocol
diff --git a/src/Main/Main.make b/src/Main/Main.make
index 36b8c1f6e9..a59ef30fc7 100755
--- a/src/Main/Main.make
+++ b/src/Main/Main.make
@@ -253,13 +253,23 @@ endif
echo -n APPLTRUE >$(APPNAME).app/Contents/PkgInfo
ifdef VC_LEGACY_BUILD
- sed -e 's/_VERSION_/$(patsubst %a,%.1,$(patsubst %b,%.2,$(TC_VERSION)))/' ../Build/Resources/MacOSX/Info.plist.legacy.xml >$(APPNAME).app/Contents/Info.plist
+ sed -e 's/_VERSION_/$(patsubst %a,%.1,$(patsubst %b,%.2,$(TC_VERSION)))/' -e 's/_TEAMID_/$(VC_OSX_TEAM_ID)/g' ../Build/Resources/MacOSX/Info.plist.legacy.xml >$(APPNAME).app/Contents/Info.plist
else
- sed -e 's/_VERSION_/$(patsubst %a,%.1,$(patsubst %b,%.2,$(TC_VERSION)))/' ../Build/Resources/MacOSX/Info.plist.xml >$(APPNAME).app/Contents/Info.plist
+ sed -e 's/_VERSION_/$(patsubst %a,%.1,$(patsubst %b,%.2,$(TC_VERSION)))/' -e 's/_TEAMID_/$(VC_OSX_TEAM_ID)/g' ../Build/Resources/MacOSX/Info.plist.xml >$(APPNAME).app/Contents/Info.plist
endif
+
+ # Build the SMJobBless privileged helper and embed it in the bundle at the
+ # location SMJobBless expects (Contents/Library/LaunchServices/