From ded55740de2c403d42b7c869af4a670b0c062ae0 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 20 May 2025 16:53:11 +0200 Subject: [PATCH 1/2] Set VSS to Auto if user has permissions If th user has the SeBackupPrivilege, set the default snapshot policy to `Auto` and otherwise use `Off` (the current default). This ensures that if the process is running with the correct permissions, there will be fewer warnings. This only changes the default value on Windows. This fixes #2833 --- Duplicati/Library/Main/Options.cs | 11 +- Duplicati/Library/Utility/PermissionHelper.cs | 266 ++++++++++++++++++ 2 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 Duplicati/Library/Utility/PermissionHelper.cs diff --git a/Duplicati/Library/Main/Options.cs b/Duplicati/Library/Main/Options.cs index 3bb038d54..8628a5320 100644 --- a/Duplicati/Library/Main/Options.cs +++ b/Duplicati/Library/Main/Options.cs @@ -109,6 +109,13 @@ namespace Duplicati.Library.Main /// private const long DEFAULT_BACKUP_TEST_SAMPLES = 1; + /// + /// The default snapshot policy + /// + private static readonly OptimizationStrategy DEFAULT_SNAPSHOT_POLICY = PermissionHelper.HasSeBackupPrivilege() + ? OptimizationStrategy.Auto + : OptimizationStrategy.Off; + /// /// The default number of compressor instances /// @@ -372,7 +379,7 @@ namespace Duplicati.Library.Main new CommandLineArgument("upload-unchanged-backups", CommandLineArgument.ArgumentType.Boolean, Strings.Options.UploadUnchangedBackupsShort, Strings.Options.UploadUnchangedBackupsLong, "false"), - new CommandLineArgument("snapshot-policy", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.SnapshotpolicyShort, Strings.Options.SnapshotpolicyLong, "off", null, Enum.GetNames(typeof(OptimizationStrategy))), + new CommandLineArgument("snapshot-policy", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.SnapshotpolicyShort, Strings.Options.SnapshotpolicyLong, DEFAULT_SNAPSHOT_POLICY.ToString(), null, Enum.GetNames(typeof(OptimizationStrategy))), new CommandLineArgument("snapshot-provider", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.SnapshotproviderShort, Strings.Options.SnapshotproviderLong, OperatingSystem.IsWindows() ? Snapshots.SnapshotProvider.AlphaVSS.ToString() : Snapshots.SnapshotProvider.LVM.ToString(), null, (OperatingSystem.IsWindows() ? [Snapshots.SnapshotProvider.AlphaVSS, Snapshots.SnapshotProvider.Wmic] : new [] { Snapshots.SnapshotProvider.LVM }).Select(x => x.ToString()).ToArray()), new CommandLineArgument("vss-exclude-writers", CommandLineArgument.ArgumentType.String, Strings.Options.VssexcludewritersShort, Strings.Options.VssexcludewritersLong, "{e8132975-6f93-4464-a53e-1050253ae220}"), new CommandLineArgument("vss-use-mapping", CommandLineArgument.ArgumentType.Boolean, Strings.Options.VssusemappingShort, Strings.Options.VssusemappingLong, "false"), @@ -894,7 +901,7 @@ namespace Duplicati.Library.Main /// /// Gets the snapshot strategy to use /// - public OptimizationStrategy SnapShotStrategy => GetEnum("snapshot-policy", OptimizationStrategy.Off); + public OptimizationStrategy SnapShotStrategy => GetEnum("snapshot-policy", DEFAULT_SNAPSHOT_POLICY); /// /// Gets the snapshot strategy to use diff --git a/Duplicati/Library/Utility/PermissionHelper.cs b/Duplicati/Library/Utility/PermissionHelper.cs new file mode 100644 index 000000000..4907fe82d --- /dev/null +++ b/Duplicati/Library/Utility/PermissionHelper.cs @@ -0,0 +1,266 @@ +#nullable enable +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Security.Principal; + +namespace Duplicati.Library.Utility; + +/// +/// Collection of helper methods to check for permissions. +/// +public static class PermissionHelper +{ + /// + /// Checks if the current process has the Backup Privilege. + /// + /// True if has Backup Privilege, false otherwise. + public static bool HasSeBackupPrivilege() + { + if (OperatingSystem.IsWindows()) + try { return HasBackupPrivilegeClaim() && PrivilegeChecker.IsPrivilegeEnabled("SeBackupPrivilege"); } + catch (DllNotFoundException) + { + // If the function is not available, we assume the privilege is not present. + } + + return false; + } + + /// + /// Check if the current process is running as an administrator, LocalSystem or root. + /// + /// True if running as administrator, LocalSystem or root, false otherwise. + public static bool IsRunningAsAdministratorOrLocalSystem() + { + if (OperatingSystem.IsWindows()) + return IsRunningAsAdministrator() || IsRunningAsLocalSystem(); + if (OperatingSystem.IsLinux()) + return IsRunningAsRoot(); + + return false; + } + + /// + /// Check if the current process is running as an administrator. + /// + /// True if running as administrator, false otherwise. + [SupportedOSPlatform("windows")] + private static bool IsRunningAsAdministrator() + { + using var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + + /// + /// Check if the current process is running as LocalSystem. + /// + /// True if running as LocalSystem, false otherwise. + [SupportedOSPlatform("windows")] + private static bool IsRunningAsLocalSystem() + { + using (var identity = WindowsIdentity.GetCurrent()) + return identity.User?.Value == "S-1-5-18"; // SID for LocalSystem + } + + /// + /// Check if the current process has the Backup Privilege. + /// + /// True if has Backup Privilege, false otherwise. + [SupportedOSPlatform("windows")] + private static bool HasBackupPrivilegeClaim() + { + using (var identity = WindowsIdentity.GetCurrent()) + foreach (var claim in identity.Claims) + if (claim.Value.Contains("SeBackupPrivilege")) + return true; + + return false; + } + + /// + /// Gets the effective user ID of the calling process. + /// + /// The effective user ID of the calling process. + [SupportedOSPlatform("linux")] + [DllImport("libc")] + public static extern uint geteuid(); + + /// + /// Check if the current process is running as root (user ID 0). + /// + /// True if running as root, false otherwise. + [SupportedOSPlatform("linux")] + private static bool IsRunningAsRoot() + { + return geteuid() == 0; + } + + /// + /// Helper class for checking Windows privileges. + /// + private class PrivilegeChecker + { + /// + /// Constants for token access rights. + /// + const int TOKEN_QUERY = 0x0008; + /// + /// Constant for privilege enabled attribute. + /// + const int SE_PRIVILEGE_ENABLED = 0x00000002; + + /// + /// Structure that contains a locally unique identifier (LUID). + /// + [StructLayout(LayoutKind.Sequential)] + struct LUID + { + public uint LowPart; + public int HighPart; + + /// + public override bool Equals(object? obj) + { + return obj is LUID other && + LowPart == other.LowPart && + HighPart == other.HighPart; + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(LowPart, HighPart); + } + } + + /// + /// Structure that contains a locally unique identifier (LUID) and its attributes. + /// + [StructLayout(LayoutKind.Sequential)] + struct LUID_AND_ATTRIBUTES + { + /// + /// The locally unique identifier (LUID) for the privilege. + /// + public LUID Luid; + /// + /// The attributes for the privilege. + /// + public int Attributes; + } + + /// + /// Structure that contains the privileges associated with a token. + /// + [StructLayout(LayoutKind.Sequential)] + struct TOKEN_PRIVILEGES + { + /// + /// The number of privileges in the Privileges array. + /// + public int PrivilegeCount; + /// + /// The privileges associated with the token. + /// + /// This field is actually an array of ANY size — we handle this dynamically + public LUID_AND_ATTRIBUTES Privileges; + } + + /// + /// Opens the access token associated with a process. + /// + /// The handle of the process whose access token is to be opened. + /// The access rights to the token. + /// A pointer to a variable that receives the handle to the opened access token. + /// true if the function succeeds; otherwise, false. + [DllImport("advapi32.dll", SetLastError = true)] + static extern bool OpenProcessToken(IntPtr ProcessHandle, int DesiredAccess, out IntPtr TokenHandle); + + /// + /// Retrieves a handle to the access token associated with the current process. + /// /// + /// A handle to the access token associated with the current process. + [DllImport("kernel32.dll")] + static extern IntPtr GetCurrentProcess(); + + /// + /// Retrieves a specified type of information about an access token. + /// + /// A handle to the access token from which information is being retrieved. + /// The type of information to be retrieved. + /// A pointer to a buffer that receives the requested information. + /// The size of the buffer pointed to by the TokenInformation parameter. + /// A pointer to a variable that receives the size of the data returned in the TokenInformation buffer. + /// true if the function succeeds; otherwise, false. + [DllImport("advapi32.dll", SetLastError = true)] + static extern bool GetTokenInformation( + IntPtr TokenHandle, + int TokenInformationClass, + IntPtr TokenInformation, + int TokenInformationLength, + out int ReturnLength + ); + + /// + /// Retrieves the locally unique identifier (LUID) for a specified privilege name. + /// + /// The name of the system on which the privilege is being looked up. This parameter can be null. + /// The name of the privilege to look up. This parameter cannot be null. + /// A pointer to a LUID structure that receives the LUID for the specified privilege. + /// true if the function succeeds; otherwise, false. + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern bool LookupPrivilegeValue(string? lpSystemName, string lpName, out LUID lpLuid); + + /// + /// Determines whether the current process token has the specified Windows privilege enabled. + /// + /// + /// The name of the privilege to check (e.g., "SeBackupPrivilege", "SeRestorePrivilege"). + /// + /// + /// true if the privilege is present and enabled in the current process token; otherwise, false. + /// + /// + /// This function does not attempt to enable the privilege, it only verifies whether it is enabled. + /// Requires that the calling process has permission to query its own token. + /// + public static bool IsPrivilegeEnabled(string privilegeName) + { + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, out IntPtr tokenHandle)) + return false; + + if (!LookupPrivilegeValue(null, privilegeName, out LUID targetLuid)) + return false; + + // First call to get required buffer size + GetTokenInformation(tokenHandle, 3, IntPtr.Zero, 0, out int bufferSize); // TokenPrivileges = 3 + IntPtr buffer = Marshal.AllocHGlobal(bufferSize); + + try + { + if (!GetTokenInformation(tokenHandle, 3, buffer, bufferSize, out _)) + return false; + + int count = Marshal.ReadInt32(buffer); + IntPtr luidPtr = buffer + sizeof(int); + int structSize = Marshal.SizeOf(); + + for (int i = 0; i < count; i++) + { + LUID_AND_ATTRIBUTES laa = Marshal.PtrToStructure(luidPtr + i * structSize); + if (laa.Luid.Equals(targetLuid)) + { + return (laa.Attributes & SE_PRIVILEGE_ENABLED) != 0; + } + } + return false; + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + } +} \ No newline at end of file From 8fb37cb6f56816be095c9f054bb1592ac382c6bb Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 20 May 2025 17:34:05 +0200 Subject: [PATCH 2/2] Fixed query to ensure privilege is enabled. Removed wrong check. --- Duplicati/Library/Utility/PermissionHelper.cs | 126 +++++++++++++++--- 1 file changed, 108 insertions(+), 18 deletions(-) diff --git a/Duplicati/Library/Utility/PermissionHelper.cs b/Duplicati/Library/Utility/PermissionHelper.cs index 4907fe82d..e27a6bb4d 100644 --- a/Duplicati/Library/Utility/PermissionHelper.cs +++ b/Duplicati/Library/Utility/PermissionHelper.cs @@ -1,4 +1,26 @@ +// Copyright (C) 2025, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + #nullable enable + using System; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -18,7 +40,10 @@ public static class PermissionHelper public static bool HasSeBackupPrivilege() { if (OperatingSystem.IsWindows()) - try { return HasBackupPrivilegeClaim() && PrivilegeChecker.IsPrivilegeEnabled("SeBackupPrivilege"); } + try + { + return PrivilegeChecker.TryEnablePrivilege("SeBackupPrivilege"); + } catch (DllNotFoundException) { // If the function is not available, we assume the privilege is not present. @@ -64,21 +89,6 @@ public static class PermissionHelper return identity.User?.Value == "S-1-5-18"; // SID for LocalSystem } - /// - /// Check if the current process has the Backup Privilege. - /// - /// True if has Backup Privilege, false otherwise. - [SupportedOSPlatform("windows")] - private static bool HasBackupPrivilegeClaim() - { - using (var identity = WindowsIdentity.GetCurrent()) - foreach (var claim in identity.Claims) - if (claim.Value.Contains("SeBackupPrivilege")) - return true; - - return false; - } - /// /// Gets the effective user ID of the calling process. /// @@ -102,6 +112,10 @@ public static class PermissionHelper /// private class PrivilegeChecker { + /// + /// Constant for adjusting token privileges + /// + const int TOKEN_ADJUST_PRIVILEGES = 0x0020; /// /// Constants for token access rights. /// @@ -213,6 +227,84 @@ public static class PermissionHelper [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool LookupPrivilegeValue(string? lpSystemName, string lpName, out LUID lpLuid); + /// + /// Adjusts the privileges of a specified access token. + /// + /// A handle to the access token whose privileges are to be adjusted. + /// Specifies whether all privileges are to be disabled. + /// A pointer to a TOKEN_PRIVILEGES structure that specifies the new privileges. + /// The size of the buffer pointed to by the PreviousState parameter. + /// A pointer to a buffer that receives the previous state of the privileges. + /// A pointer to a variable that receives the size of the data returned in the PreviousState buffer. + /// true if the function succeeds; otherwise, false. + [DllImport("advapi32.dll", SetLastError = true)] + static extern bool AdjustTokenPrivileges( + IntPtr TokenHandle, + bool DisableAllPrivileges, + ref TOKEN_PRIVILEGES NewState, + int BufferLength, + IntPtr PreviousState, + IntPtr ReturnLength + ); + + /// + /// Retrieves the calling thread's last-error code value. + /// + /// The last-error code value. + [DllImport("kernel32.dll")] + static extern uint GetLastError(); + + /// + /// Flag tracking if backup privilege is enabled + /// + private static bool? _isPrivilegeEnabled; + + /// + /// Tries to enable a specific privilege in the current process token. + /// + /// The name of the privilege (e.g., "SeBackupPrivilege"). + /// + /// True if the privilege is present and was successfully enabled. + /// False if the privilege is not held or could not be enabled. + /// + public static bool TryEnablePrivilege(string privilegeName) + { + if (_isPrivilegeEnabled.HasValue) + return _isPrivilegeEnabled.Value; + + try + { + _isPrivilegeEnabled = false; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out IntPtr tokenHandle)) + return false; + + if (!LookupPrivilegeValue(null, privilegeName, out LUID luid)) + return false; + + var tp = new TOKEN_PRIVILEGES + { + PrivilegeCount = 1, + Privileges = new LUID_AND_ATTRIBUTES() + { + Luid = luid, + Attributes = SE_PRIVILEGE_ENABLED + } + }; + + if (!AdjustTokenPrivileges(tokenHandle, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero)) + return false; + + _isPrivilegeEnabled = (GetLastError() == 0); // If not zero, then the privilege wasn't assigned (e.g., ERROR_NOT_ALL_ASSIGNED) + return _isPrivilegeEnabled.Value; + } + catch (Exception) + { + // Ignore this error + } + + return _isPrivilegeEnabled ?? false; + } + /// /// Determines whether the current process token has the specified Windows privilege enabled. /// @@ -251,9 +343,7 @@ public static class PermissionHelper { LUID_AND_ATTRIBUTES laa = Marshal.PtrToStructure(luidPtr + i * structSize); if (laa.Luid.Equals(targetLuid)) - { return (laa.Attributes & SE_PRIVILEGE_ENABLED) != 0; - } } return false; }