Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 72 additions & 85 deletions res/msi/CustomActions/CustomActions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ UINT __stdcall CustomActionHello(
return WcaFinalize(er);
}

// Helper function to safely delete a file or directory using handle-based deletion.
// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions.
// Helper function to safely delete a file using handle-based deletion.
// Directories are refused after opening the handle.
BOOL SafeDeleteItem(LPCWSTR fullPath)
{
// Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT
// Open the file/directory with delete and attribute-read access plus FILE_FLAG_OPEN_REPARSE_POINT
// to prevent following symlinks.
// Use shared access to allow deletion even when other processes have the file open.
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT;
HANDLE hFile = CreateFileW(
fullPath,
DELETE,
DELETE | FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access
NULL,
OPEN_EXISTING,
Expand All @@ -55,6 +55,21 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
return FALSE;
}

BY_HANDLE_FILE_INFORMATION fileInfo;
if (FALSE == GetFileInformationByHandle(hFile, &fileInfo))
{
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to inspect '%ls'. Error: %lu", fullPath, GetLastError());
CloseHandle(hFile);
return FALSE;
}

if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Refusing to delete directory '%ls'.", fullPath);
CloseHandle(hFile);
return FALSE;
}

// Use SetFileInformationByHandle to mark for deletion.
// The file will be deleted when the handle is closed.
FILE_DISPOSITION_INFO dispInfo;
Expand All @@ -77,98 +92,74 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
return result;
}

// Helper function to recursively delete a directory's contents with detailed logging.
void RecursiveDelete(LPCWSTR path)
BOOL PathEndsWithSlash(LPCWSTR path)
{
// Ensure the path is not empty or null.
if (path == NULL || path[0] == L'\0')
size_t length = 0;
HRESULT hr = StringCchLengthW(path, MAX_PATH, &length);
if (FAILED(hr) || length == 0)
{
return;
return FALSE;
}

// Extra safety: never operate directly on a root path.
if (PathIsRootW(path))
WCHAR last = path[length - 1];
return last == L'\\' || last == L'/';
}

void ClearReadOnlyAttribute(LPCWSTR fullPath, DWORD attributes)
{
if (!(attributes & FILE_ATTRIBUTE_READONLY))
{
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path);
return;
}

// MAX_PATH is enough here since the installer should not be using longer paths.
// No need to handle extended-length paths (\\?\) in this context.
WCHAR searchPath[MAX_PATH];
HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path);
if (FAILED(hr)) {
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path);
return;
DWORD writableAttributes = attributes & ~FILE_ATTRIBUTE_READONLY;
if (writableAttributes == 0)
{
writableAttributes = FILE_ATTRIBUTE_NORMAL;
}

WIN32_FIND_DATAW findData;
HANDLE hFind = FindFirstFileW(searchPath, &findData);

if (hFind == INVALID_HANDLE_VALUE)
if (SetFileAttributesW(fullPath, writableAttributes))
{
// This can happen if the directory is empty or doesn't exist, which is not an error in our case.
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError());
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cleared read-only attribute for '%ls'.", fullPath);
return;
}

do
{
// Skip '.' and '..' directories.
if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
{
continue;
}
WcaLog(LOGMSG_STANDARD, "Runtime cleanup failed to clear read-only attribute for '%ls'. Error: %lu", fullPath, GetLastError());
}

// MAX_PATH is enough here since the installer should not be using longer paths.
// No need to handle extended-length paths (\\?\) in this context.
WCHAR fullPath[MAX_PATH];
hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName);
if (FAILED(hr)) {
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path);
continue;
}
BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName)
{
WCHAR fullPath[MAX_PATH];
LPCWSTR separator = PathEndsWithSlash(installFolder) ? L"" : L"\\";
HRESULT hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s%s%s", installFolder, separator, fileName);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Runtime cleanup path is too long for '%ls'.", fileName);
return FALSE;
}

// Before acting, ensure the read-only attribute is not set.
if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
DWORD attributes = GetFileAttributesW(fullPath);
if (attributes == INVALID_FILE_ATTRIBUTES)
{
DWORD error = GetLastError();
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND)
{
if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY))
{
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError());
}
return TRUE;
}

if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
// Check for reparse points (symlinks/junctions) to prevent directory traversal attacks.
// Do not follow reparse points, only remove the link itself.
if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
{
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath);
SafeDeleteItem(fullPath);
}
else
{
// Recursively delete directory contents first
RecursiveDelete(fullPath);
// Then delete the directory itself
SafeDeleteItem(fullPath);
}
}
else
{
// Delete file using safe handle-based deletion
SafeDeleteItem(fullPath);
}
} while (FindNextFileW(hFind, &findData) != 0);
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cannot stat '%ls'. Error: %lu", fullPath, error);
return FALSE;
}

DWORD lastError = GetLastError();
if (lastError != ERROR_NO_MORE_FILES)
if (attributes & FILE_ATTRIBUTE_DIRECTORY)
{
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError);
WcaLog(LOGMSG_STANDARD, "Runtime cleanup skipped directory '%ls'.", fullPath);
return FALSE;
}

FindClose(hFind);
ClearReadOnlyAttribute(fullPath, attributes);
WcaLog(LOGMSG_STANDARD, "Runtime cleanup deleting '%ls'.", fullPath);
return SafeDeleteItem(fullPath);
}

// See `Package.wxs` for the sequence of this custom action.
Expand All @@ -178,13 +169,13 @@ void RecursiveDelete(LPCWSTR path)
// 2. RemoveExistingProducts
// ├─ TerminateProcesses
// ├─ TryStopDeleteService
// ├─ RemoveInstallFolder - <-- Here
// ├─ RemoveRuntimeGeneratedFiles - <-- Here
// └─ RemoveFiles
// 3. InstallValidate
// 4. InstallFiles
// 5. InstallExecute
// 6. InstallFinalize
UINT __stdcall RemoveInstallFolder(
UINT __stdcall RemoveRuntimeGeneratedFiles(
__in MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
Expand All @@ -194,32 +185,28 @@ UINT __stdcall RemoveInstallFolder(
LPWSTR pwz = NULL;
LPWSTR pwzData = NULL;

hr = WcaInitialize(hInstall, "RemoveInstallFolder");
hr = WcaInitialize(hInstall, "RemoveRuntimeGeneratedFiles");
ExitOnFailure(hr, "Failed to initialize");

hr = WcaGetProperty(L"CustomActionData", &pwzData);
ExitOnFailure(hr, "failed to get CustomActionData");

pwz = pwzData;
hr = WcaReadStringFromCaData(&pwz, &installFolder);
ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
ExitOnFailure(hr, "failed to read install folder from custom action data: %ls", pwz);

if (installFolder == NULL || installFolder[0] == L'\0') {
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete.");
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping runtime cleanup.");
goto LExit;
}

if (PathIsRootW(installFolder)) {
WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder);
WcaLog(LOGMSG_STANDARD, "Refusing runtime cleanup in root folder '%ls'.", installFolder);
goto LExit;
}

WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder);

RecursiveDelete(installFolder);

// The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories.
// We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer.
WcaLog(LOGMSG_STANDARD, "Removing runtime-generated files from install folder: %ls", installFolder);
DeleteRuntimeGeneratedFile(installFolder, L"RuntimeBroker_rustdesk.exe");

LExit:
ReleaseStr(pwzData);
Expand Down
2 changes: 1 addition & 1 deletion res/msi/CustomActions/CustomActions.def
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ LIBRARY "CustomActions"

EXPORTS
CustomActionHello
RemoveInstallFolder
RemoveRuntimeGeneratedFiles
TerminateProcesses
AddFirewallRules
SetPropertyIsServiceRunning
Expand Down
11 changes: 9 additions & 2 deletions res/msi/Package/Components/Folders.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,15 @@
<!-- If a command line value was stored, restore it after the registry search has been performed -->
<SetProperty Action="RestoreSavedInstallFolderValue" Id="INSTALLFOLDER" Value="[SavedInstallFolderCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedInstallFolderCmdLineValue" />

<!-- If a command line value or registry value was set, update the main properties with the value -->
<SetProperty Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER" />
<!-- Normalize INSTALLFOLDER from the command line or registry before assigning INSTALLFOLDER_INNER. -->
<!-- Case 1: already ends with \$(var.Product)\, keep it unchanged. -->
<SetProperty Action="SetInstallFolderInnerFromProductDir" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot;" />
<!-- Case 2: already ends with \$(var.Product) but has no trailing slash, add the slash. -->
<SetProperty Action="SetInstallFolderInnerFromProductDirNoSlash" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;" />
<!-- Case 3: ends with a slash but not \$(var.Product)\, append $(var.Product)\. -->
<SetProperty Action="SetInstallFolderInnerAppendProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<!-- Case 4: has no trailing slash and does not end with \$(var.Product), append \$(var.Product)\. -->
<SetProperty Action="SetInstallFolderInnerAppendSlashProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND NOT INSTALLFOLDER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />

<!-- INSTALLFOLDER_INNER is defined for compatibility with previous versions of the installer. -->
<!-- Because we need to use INSTALLFOLDER as the command line argument. -->
Expand Down
16 changes: 8 additions & 8 deletions res/msi/Package/Components/RustDesk.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</Component>
</DirectoryRef>

<CustomAction Id="RemoveInstallFolder.SetParam" Return="check" Property="RemoveInstallFolder" Value="[INSTALLFOLDER_INNER]" />
<CustomAction Id="RemoveRuntimeGeneratedFiles.SetParam" Return="check" Property="RemoveRuntimeGeneratedFiles" Value="[INSTALLFOLDER_INNER]" />
<CustomAction Id="AddFirewallRules.SetParam" Return="check" Property="AddFirewallRules" Value="1[INSTALLFOLDER_INNER]$(var.Product).exe" />
<CustomAction Id="RemoveFirewallRules.SetParam" Return="check" Property="RemoveFirewallRules" Value="0[INSTALLFOLDER_INNER]$(var.Product).exe" />
<CustomAction Id="CreateStartService.SetParam" Return="check" Property="CreateStartService" Value="$(var.Product);&quot;[INSTALLFOLDER_INNER]$(var.Product).exe&quot; --service" />
Expand Down Expand Up @@ -77,21 +77,21 @@

<Custom Action="AddRegSoftwareSASGeneration" Before="InstallFinalize" Condition="NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE) AND (NOT CC_CONNECTION_TYPE=&quot;outgoing&quot;)"/>

<Custom Action="RemoveInstallFolder" Before="RemoveFiles"/>
<Custom Action="RemoveInstallFolder.SetParam" Before="RemoveInstallFolder"/>
<Custom Action="TryStopDeleteService" Before="RemoveInstallFolder.SetParam" />
<Custom Action="RemoveRuntimeGeneratedFiles" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot; OR UPGRADINGPRODUCTCODE)"/>
<Custom Action="RemoveRuntimeGeneratedFiles.SetParam" Before="RemoveRuntimeGeneratedFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot; OR UPGRADINGPRODUCTCODE)"/>
<Custom Action="TryStopDeleteService" Before="RemoveRuntimeGeneratedFiles.SetParam" />
<Custom Action="TryStopDeleteService.SetParam" Before="TryStopDeleteService" />

<Custom Action="RemoveFirewallRules" Before="RemoveFiles"/>
<Custom Action="RemoveFirewallRules.SetParam" Before="RemoveFirewallRules"/>

<Custom Action="UninstallPrinter" Before="RemoveInstallFolder" Condition="VersionNT &gt;= 603" />
<Custom Action="UninstallPrinter" Before="RemoveRuntimeGeneratedFiles" Condition="VersionNT &gt;= 603" />

<Custom Action="TerminateProcesses" Before="RemoveInstallFolder"/>
<Custom Action="TerminateProcesses" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="TerminateProcesses.SetParam" Before="TerminateProcesses"/>
<Custom Action="TerminateBrokers" Before="RemoveInstallFolder"/>
<Custom Action="TerminateBrokers" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="TerminateBrokers.SetParam" Before="TerminateBrokers"/>
<Custom Action="RemoveAmyuniIdd" Before="RemoveInstallFolder"/>
<Custom Action="RemoveAmyuniIdd" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="RemoveAmyuniIdd.SetParam" Before="RemoveAmyuniIdd"/>
</InstallExecuteSequence>

Expand Down
2 changes: 1 addition & 1 deletion res/msi/Package/Fragments/CustomActions.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Binary Id="Custom_Actions_Dll" SourceFile="$(var.CustomActions.TargetDir)$(var.CustomActions.TargetName).dll" />

<CustomAction Id="CustomActionHello" DllEntry="CustomActionHello" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="RemoveInstallFolder" DllEntry="RemoveInstallFolder" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="RemoveRuntimeGeneratedFiles" DllEntry="RemoveRuntimeGeneratedFiles" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="TerminateProcesses" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="TerminateBrokers" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="AddFirewallRules" DllEntry="AddFirewallRules" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
Expand Down
Loading
Loading