Files
duplicati/Duplicati/Library/Utility/PermissionHelper.cs
T
Kenneth Skovhede 977bb88263 Check if VCRedist is missing on VSS fail
This PR adds a small check if the VSS provider fails to initialize. We already checked if the user appears to have the `SeBackup` privilege and tailor the message to that.

With this PR we now also factor in a potentially missing VC Redist package, such that the user is informed about this package not being found.

The intention is to point towards the most likely cause of the failure. If VSS works, or not running on Windows, nothing has changed.
2026-07-02 20:45:18 +02:00

476 lines
20 KiB
C#

// Copyright (C) 2026, 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;
using System.Security.Principal;
using Microsoft.Win32;
namespace Duplicati.Library.Utility;
/// <summary>
/// Collection of helper methods to check for permissions.
/// </summary>
public static class PermissionHelper
{
/// <summary>
/// Checks if the current process has the required privileges to create snapshots.
/// Note that this just checks the SeBackupPrivilege or if the process is running as root.
/// There are other ways to give snapshot permissions, but this seems like a good indicator.
/// </summary>
/// <returns><c>true</c> if the current process has the required privileges to create snapshots, <c>false</c> otherwise.</returns>
public static bool HasSnapshotPrivilege()
{
try
{
return OperatingSystem.IsWindows()
? HasSeBackupPrivilege()
: IsRunningAsAdministratorOrLocalSystem();
}
catch
{ }
return false;
}
/// <summary>
/// Checks if the Microsoft Visual C++ Redistributable matching the current process architecture
/// (x64 or x86) is installed on the machine. The VC++ 2015-2022 runtime ships architecture-specific
/// binaries (VCRUNTIME140.dll / VCRUNTIME140_1.dll), and the VSS snapshot providers load mixed-mode
/// C++/CLI assemblies that link against them, so the check must match the running process architecture.
/// </summary>
/// <returns>
/// <c>true</c> if a matching Visual C++ Redistributable package is installed, <c>false</c> otherwise.
/// Always returns <c>true</c> on non-Windows platforms where the check does not apply.
/// </returns>
public static bool IsVisualCRedistInstalled()
{
if (!OperatingSystem.IsWindows())
return true;
// The VC++ 2015-2022 runtime uses architecture-specific registry subkey names.
// For older runtimes (VC++ 2010/2013) the key lives elsewhere, but the mixed-mode
// snapshot assemblies are built against the VC++ 2015-2022 toolset, so checking
// the 14.0 key for the matching architecture is the relevant test.
var arch = RuntimeInformation.ProcessArchitecture switch
{
Architecture.X86 => "x86",
Architecture.X64 => "x64",
Architecture.Arm64 => "arm64",
_ => "x64",
};
// The VC++ Runtimes keys are not WOW6432Node-redirected, so both registry views
// resolve to the same physical location. RegistryView.Default follows the
// process architecture
try
{
using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Default);
return HasVisualCRedistInRegistry(baseKey, $@"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\{arch}");
}
catch
{
// Fail open: if registry access itself fails we cannot reliably determine
// whether the redistributable is installed, so we report it as present
// rather than misdirecting the user toward an irrelevant install step.
return true;
}
}
/// <summary>
/// Checks if a Visual C++ Redistributable runtime entry exists in the given registry location.
/// </summary>
/// <param name="root">The registry root key.</param>
/// <param name="path">The path to the runtime registry key.</param>
/// <returns><c>true</c> if the entry exists and reports an installed version, <c>false</c> otherwise.</returns>
[SupportedOSPlatform("windows")]
private static bool HasVisualCRedistInRegistry(RegistryKey root, string path)
{
using var key = root.OpenSubKey(path);
if (key == null)
return false;
// The "Installed" value is a DWORD (1 = installed). Some installers only write "Version".
return key.GetValue("Installed") is int installed && installed != 0
|| key.GetValue("Version") != null;
}
/// <summary>
/// Checks if the current process has the Backup Privilege.
/// </summary>
/// <returns>True if has Backup Privilege, false otherwise.</returns>
public static bool HasSeBackupPrivilege()
{
if (OperatingSystem.IsWindows())
try
{
return PrivilegeChecker.TryEnablePrivilege("SeBackupPrivilege");
}
catch (DllNotFoundException)
{
// If the function is not available, we assume the privilege is not present.
}
return false;
}
/// <summary>
/// Check if the current process is running as an administrator, LocalSystem or root.
/// </summary>
/// <returns>True if running as administrator, LocalSystem or root, false otherwise.</returns>
public static bool IsRunningAsAdministratorOrLocalSystem()
{
if (OperatingSystem.IsWindows())
return IsRunningAsAdministrator() || IsRunningAsLocalSystem();
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
return IsRunningAsRoot();
return false;
}
/// <summary>
/// Check if the current process is running as an administrator.
/// </summary>
/// <returns>True if running as administrator, false otherwise.</returns>
[SupportedOSPlatform("windows")]
private static bool IsRunningAsAdministrator()
{
using var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
/// <summary>
/// Check if the current process is running as LocalSystem.
/// </summary>
/// <returns>True if running as LocalSystem, false otherwise.</returns>
[SupportedOSPlatform("windows")]
private static bool IsRunningAsLocalSystem()
{
using (var identity = WindowsIdentity.GetCurrent())
return identity.User?.Value == "S-1-5-18"; // SID for LocalSystem
}
/// <summary>
/// Gets the effective user ID of the calling process.
/// </summary>
/// <returns>The effective user ID of the calling process.</returns>
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
[DllImport("libc")]
public static extern uint geteuid();
/// <summary>
/// Check if the current process is running as root (user ID 0).
/// </summary>
/// <returns>True if running as root, false otherwise.</returns>
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
private static bool IsRunningAsRoot()
{
return geteuid() == 0;
}
/// <summary>
/// Helper class for checking Windows privileges.
/// </summary>
private class PrivilegeChecker
{
/// <summary>
/// Constant for adjusting token privileges
/// </summary>
const int TOKEN_ADJUST_PRIVILEGES = 0x0020;
/// <summary>
/// Constants for token access rights.
/// </summary>
const int TOKEN_QUERY = 0x0008;
/// <summary>
/// Constant for privilege enabled attribute.
/// </summary>
const int SE_PRIVILEGE_ENABLED = 0x00000002;
/// <summary>
/// The TOKEN_INFORMATION_CLASS.TokenPrivileges value
/// </summary>
const int TOKEN_INFORMATION_TOKEN_PRIVILEGES = 3;
/// <summary>
/// Structure that contains a locally unique identifier (LUID).
/// </summary>
[StructLayout(LayoutKind.Sequential)]
struct LUID
{
public uint LowPart;
public int HighPart;
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is LUID other &&
LowPart == other.LowPart &&
HighPart == other.HighPart;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(LowPart, HighPart);
}
}
/// <summary>
/// Structure that contains a locally unique identifier (LUID) and its attributes.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
struct LUID_AND_ATTRIBUTES
{
/// <summary>
/// The locally unique identifier (LUID) for the privilege.
/// </summary>
public LUID Luid;
/// <summary>
/// The attributes for the privilege.
/// </summary>
public int Attributes;
}
/// <summary>
/// Structure that contains the privileges associated with a token.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
struct TOKEN_PRIVILEGES
{
/// <summary>
/// The number of privileges in the Privileges array.
/// </summary>
public int PrivilegeCount;
/// <summary>
/// The privileges associated with the token.
/// </summary>
/// <remarks>This field is actually an array of ANY size — we handle this dynamically</remarks>
public LUID_AND_ATTRIBUTES Privileges;
}
/// <summary>
/// Opens the access token associated with a process.
/// </summary>
/// <param name="ProcessHandle">The handle of the process whose access token is to be opened.</param>
/// <param name="DesiredAccess">The access rights to the token.</param>
/// <param name="TokenHandle"> A pointer to a variable that receives the handle to the opened access token.</param>
/// <returns><c>true</c> if the function succeeds; otherwise, <c>false</c>.</returns>
[DllImport("advapi32.dll", SetLastError = true)]
static extern bool OpenProcessToken(IntPtr ProcessHandle, int DesiredAccess, out IntPtr TokenHandle);
/// <summary>
/// Retrieves a handle to the access token associated with the current process.
/// /// </summary>
/// <returns>A handle to the access token associated with the current process.</returns>
[DllImport("kernel32.dll")]
static extern IntPtr GetCurrentProcess();
/// <summary>
/// Retrieves a specified type of information about an access token.
/// </summary>
/// <param name="TokenHandle">A handle to the access token from which information is being retrieved.</param>
/// <param name="TokenInformationClass">The type of information to be retrieved.</param>
/// <param name="TokenInformation">A pointer to a buffer that receives the requested information.</param>
/// <param name="TokenInformationLength">The size of the buffer pointed to by the TokenInformation parameter.</param>
/// <param name="ReturnLength">A pointer to a variable that receives the size of the data returned in the TokenInformation buffer.</param>
/// <returns><c>true</c> if the function succeeds; otherwise, <c>false</c>.</returns>
[DllImport("advapi32.dll", SetLastError = true)]
static extern bool GetTokenInformation(
IntPtr TokenHandle,
int TokenInformationClass,
IntPtr TokenInformation,
int TokenInformationLength,
out int ReturnLength
);
/// <summary>
/// Retrieves the locally unique identifier (LUID) for a specified privilege name.
/// </summary>
/// <param name="lpSystemName">The name of the system on which the privilege is being looked up. This parameter can be null.</param>
/// <param name="lpName">The name of the privilege to look up. This parameter cannot be null.</param>
/// <param name="lpLuid">A pointer to a LUID structure that receives the LUID for the specified privilege.</param>
/// <returns><c>true</c> if the function succeeds; otherwise, <c>false</c>.</returns>
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool LookupPrivilegeValue(string? lpSystemName, string lpName, out LUID lpLuid);
/// <summary>
/// Adjusts the privileges of a specified access token.
/// </summary>
/// <param name="TokenHandle">A handle to the access token whose privileges are to be adjusted.</param>
/// <param name="DisableAllPrivileges">Specifies whether all privileges are to be disabled.</param>
/// <param name="NewState">A pointer to a TOKEN_PRIVILEGES structure that specifies the new privileges.</param>
/// <param name="BufferLength">The size of the buffer pointed to by the PreviousState parameter.</param>
/// <param name="PreviousState">A pointer to a buffer that receives the previous state of the privileges.</param>
/// <param name="ReturnLength">A pointer to a variable that receives the size of the data returned in the PreviousState buffer.</param>
/// <returns><c>true</c> if the function succeeds; otherwise, <c>false</c>.</returns>
[DllImport("advapi32.dll", SetLastError = true)]
static extern bool AdjustTokenPrivileges(
IntPtr TokenHandle,
bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState,
int BufferLength,
IntPtr PreviousState,
out uint ReturnLength
);
/// <summary>
/// Retrieves the calling thread's last-error code value.
/// </summary>
/// <returns>The last-error code value.</returns>
[DllImport("kernel32.dll")]
static extern uint GetLastError();
/// <summary>
/// Closes an open object handle.
/// </summary>
/// <param name="hObject">The handle to the object to be closed.</param>
/// <returns><c>true</c> if the function succeeds; otherwise, <c>false</c>.</returns>
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
/// <summary>
/// Flag tracking if backup privilege is enabled
/// </summary>
private static bool? _isPrivilegeEnabled;
/// <summary>
/// Tries to enable a specific privilege in the current process token.
/// </summary>
/// <param name="privilegeName">The name of the privilege (e.g., "SeBackupPrivilege").</param>
/// <returns>
/// True if the privilege is present and was successfully enabled.
/// False if the privilege is not held or could not be enabled.
/// </returns>
public static bool TryEnablePrivilege(string privilegeName)
{
if (_isPrivilegeEnabled.HasValue)
return _isPrivilegeEnabled.Value;
_isPrivilegeEnabled = false;
// Open the primary token for this process
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out var hTok))
return false;
try
{
// Translate the textual privilege name (e.g. "SeBackupPrivilege") to a LUID
if (!LookupPrivilegeValue(null, privilegeName, out var luid))
return false;
// Build TOKEN_PRIVILEGES {1 entry, SE_PRIVILEGE_ENABLED}
var tp = new TOKEN_PRIVILEGES
{
PrivilegeCount = 1,
Privileges = new LUID_AND_ATTRIBUTES
{
Luid = luid,
Attributes = SE_PRIVILEGE_ENABLED
}
};
// Query the size of the current privileges set
GetTokenInformation(hTok, TOKEN_INFORMATION_TOKEN_PRIVILEGES, IntPtr.Zero, 0, out int requiredLen);
// Allocate a buffer of the requested size
var prevStatePtr = Marshal.AllocHGlobal(requiredLen);
try
{
// Enable the privilege, capturing the previous state
if (!AdjustTokenPrivileges(hTok, false, ref tp, requiredLen, prevStatePtr, out var _))
return false;
// If GetLastError() == ERROR_SUCCESS (0), the privilege is now enabled
_isPrivilegeEnabled = Marshal.GetLastWin32Error() == 0;
// Restore the original state
var oldState = Marshal.PtrToStructure<TOKEN_PRIVILEGES>(prevStatePtr);
AdjustTokenPrivileges(hTok, false, ref oldState, 0, IntPtr.Zero, out var _);
}
finally
{
Marshal.FreeHGlobal(prevStatePtr);
}
return _isPrivilegeEnabled.Value;
}
finally
{
CloseHandle(hTok);
}
}
/// <summary>
/// Determines whether the current process token has the specified Windows privilege enabled.
/// </summary>
/// <param name="privilegeName">
/// The name of the privilege to check (e.g., "SeBackupPrivilege", "SeRestorePrivilege").
/// </param>
/// <returns>
/// <c>true</c> if the privilege is present and enabled in the current process token; otherwise, <c>false</c>.
/// </returns>
/// <remarks>
/// 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.
/// </remarks>
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<LUID_AND_ATTRIBUTES>();
for (int i = 0; i < count; i++)
{
LUID_AND_ATTRIBUTES laa = Marshal.PtrToStructure<LUID_AND_ATTRIBUTES>(luidPtr + i * structSize);
if (laa.Luid.Equals(targetLuid))
return (laa.Attributes & SE_PRIVILEGE_ENABLED) != 0;
}
return false;
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}
}
}