From 681cf4a9eb6be7e4092c6e5b690773fbd8469e63 Mon Sep 17 00:00:00 2001
From: Rob Mensching <rob@firegiant.com>
Date: Thu, 4 Apr 2024 15:24:34 -0700
Subject: Ensure elevated SFXCA uses Windows Installer cache and unelevated
 uses Temp folder

Fixes 8078
---
 src/dtf/SfxCA/SfxCA.vcxproj                        |   2 +-
 src/dtf/SfxCA/SfxUtil.cpp                          | 169 +++++++++++++++++----
 src/dtf/dtf.cmd                                    |  23 +++
 .../ManagedCustomActions/Package.wxs               |   6 +-
 .../CustomActionTests/TestCA/CustomAction.cs       |  38 +++++
 5 files changed, 203 insertions(+), 35 deletions(-)

(limited to 'src')

diff --git a/src/dtf/SfxCA/SfxCA.vcxproj b/src/dtf/SfxCA/SfxCA.vcxproj
index 5c4f674f..ee591619 100644
--- a/src/dtf/SfxCA/SfxCA.vcxproj
+++ b/src/dtf/SfxCA/SfxCA.vcxproj
@@ -41,7 +41,7 @@
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
 
   <PropertyGroup>
-    <ProjectAdditionalLinkLibraries>msi.lib;cabinet.lib;shlwapi.lib</ProjectAdditionalLinkLibraries>
+    <ProjectAdditionalLinkLibraries>msi.lib;cabinet.lib;rpcrt4.lib;shlwapi.lib</ProjectAdditionalLinkLibraries>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/src/dtf/SfxCA/SfxUtil.cpp b/src/dtf/SfxCA/SfxUtil.cpp
index 32dc6e04..079f1617 100644
--- a/src/dtf/SfxCA/SfxUtil.cpp
+++ b/src/dtf/SfxCA/SfxUtil.cpp
@@ -3,6 +3,8 @@
 #include "precomp.h"
 #include "SfxUtil.h"
 
+#define GUID_STRING_LENGTH 39
+
 /// <summary>
 /// Writes a formatted message to the MSI log.
 /// Does out-of-proc MSI calls if necessary.
@@ -110,20 +112,75 @@ bool DeleteDirectory(const wchar_t* szDir)
                         hSearch = INVALID_HANDLE_VALUE;
                 }
         }
-        return RemoveDirectory(szDir) != 0;
+
+        for (int i = 0; i < 3; i++)
+        {
+            if (::RemoveDirectory(szDir))
+            {
+                return true;
+            }
+
+            ::Sleep(100);
+        }
+
+        return false;
 }
 
-bool DirectoryExists(const wchar_t* szDir)
+static HRESULT CreateGuid(
+    _Out_z_cap_c_(GUID_STRING_LENGTH) wchar_t* wzGuid)
 {
-        if (szDir != NULL)
+        HRESULT hr = S_OK;
+        RPC_STATUS rs = RPC_S_OK;
+        UUID guid = {};
+
+        rs = ::UuidCreate(&guid);
+        if (rs != RPC_S_OK)
         {
-                DWORD dwAttrs = GetFileAttributes(szDir);
-                if (dwAttrs != -1 && (dwAttrs & FILE_ATTRIBUTE_DIRECTORY) != 0)
-                {
-                        return true;
-                }
+                hr = (HRESULT)(rs | FACILITY_RPC);
         }
-        return false;
+        else if (!::StringFromGUID2(guid, wzGuid, GUID_STRING_LENGTH))
+        {
+                hr = E_OUTOFMEMORY;
+        }
+        else // make the temp directory more recognizable for easy deletion.
+        {
+                // Copy the first four hex chars of the GUID over the dashes in the GUID and trim the, so
+                // '{1234ABCD-ABCD-ABCD-ABCD-ABCDABCDABCD}' turns into '{1234ABCD1ABCD2ABCD3ABCD4ABCDABCDABCD}'
+                wzGuid[9] = wzGuid[1];
+                wzGuid[14] = wzGuid[2];
+                wzGuid[19] = wzGuid[3];
+                wzGuid[24] = wzGuid[4];
+
+                // Now '{1234ABCD1ABCD2ABCD3ABCD4ABCDABCDABCD}' turns into 'SFXCAABCD1ABCD2ABCD3ABCD4ABCDABCDABCD'
+                wzGuid[0] = L'S';
+                wzGuid[1] = L'F';
+                wzGuid[2] = L'X';
+                wzGuid[3] = L'C';
+                wzGuid[4] = L'A';
+                wzGuid[GUID_STRING_LENGTH - 2] = L'\0';
+        }
+
+        return hr;
+}
+
+static HRESULT ProcessElevated()
+{
+        HRESULT hr = S_OK;
+        HANDLE hToken = NULL;
+        TOKEN_ELEVATION tokenElevated = {};
+        DWORD cbToken = 0;
+
+        if (::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &hToken) &&
+            ::GetTokenInformation(hToken, TokenElevation, &tokenElevated, sizeof(TOKEN_ELEVATION), &cbToken))
+        {
+                hr = (0 != tokenElevated.TokenIsElevated) ? S_OK : S_FALSE;
+        }
+        else
+        {
+                hr = HRESULT_FROM_WIN32(::GetLastError());
+        }
+
+        return hr;
 }
 
 /// <summary>
@@ -143,49 +200,95 @@ __success(return != false)
 bool ExtractToTempDirectory(__in MSIHANDLE hSession, __in HMODULE hModule,
         __out_ecount_z(cchTempDirBuf) wchar_t* szTempDir, DWORD cchTempDirBuf)
 {
-        wchar_t szModule[MAX_PATH];
-        DWORD cchCopied = GetModuleFileName(hModule, szModule, MAX_PATH - 1);
-        if (cchCopied == 0)
+        HRESULT hr = S_OK;
+        wchar_t szModule[MAX_PATH] = {};
+        wchar_t szGuid[GUID_STRING_LENGTH] = {};
+
+        DWORD cchCopied = ::GetModuleFileName(hModule, szModule, MAX_PATH - 1);
+        if (cchCopied == 0 || cchCopied == MAX_PATH - 1)
         {
-                Log(hSession, L"Failed to get module path. Error code %d.", GetLastError());
-                return false;
+                hr = HRESULT_FROM_WIN32(::GetLastError());
+                if (SUCCEEDED(hr))
+                {
+                        hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
+                }
+
+                Log(hSession, L"Failed to get module path. Error code 0x%x.", hr);
+                goto LExit;
         }
-        else if (cchCopied == MAX_PATH - 1)
+
+        hr = CreateGuid(szGuid);
+        if (FAILED(hr))
         {
-                Log(hSession, L"Failed to get module path -- path is too long.");
-                return false;
+                Log(hSession, L"Failed to create a GUID. Error code 0x%x", hr);
+                goto LExit;
         }
 
-        if (szTempDir == NULL || cchTempDirBuf < wcslen(szModule) + 1)
+        // Unelevated we use the user's temp directory.
+        hr = ProcessElevated();
+        if (S_FALSE == hr)
+        {
+                // Temp path is documented to be returned with a trailing backslash.
+                cchCopied = ::GetTempPath(cchTempDirBuf, szTempDir);
+                if (cchCopied == 0 || cchCopied >= cchTempDirBuf)
+                {
+                        hr = HRESULT_FROM_WIN32(::GetLastError());
+                        if (SUCCEEDED(hr))
+                        {
+                                hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
+                        }
+
+                        Log(hSession, L"Failed to get user temp directory. Error code 0x%x", hr);
+                        goto LExit;
+                }
+        }
+        else // elevated or we couldn't check (in the latter case, assume we're elevated since it's safer to use)
         {
-                Log(hSession, L"Temp directory buffer is NULL or too small.");
-                return false;
+                // Windows directory will not contain a trailing backslash, so we add it next.
+                cchCopied = ::GetWindowsDirectoryW(szTempDir, cchTempDirBuf);
+                if (cchCopied == 0 || cchCopied >= cchTempDirBuf)
+                {
+                        hr = HRESULT_FROM_WIN32(::GetLastError());
+                        if (SUCCEEDED(hr))
+                        {
+                                hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
+                        }
+
+                        Log(hSession, L"Failed to get Windows directory. Error code 0x%x", hr);
+                        goto LExit;
+                }
+
+                hr = ::StringCchCat(szTempDir, cchTempDirBuf, L"\\Installer\\");
+                if (FAILED(hr))
+                {
+                        Log(hSession, L"Failed append 'Installer' to Windows directory. Error code 0x%x", hr);
+                        goto LExit;
+                }
         }
-        StringCchCopy(szTempDir, cchTempDirBuf, szModule);
-        StringCchCat(szTempDir, cchTempDirBuf, L"-");
 
-        BOOL fCreatedDirectory = FALSE;
-        DWORD cchTempDir = (DWORD) wcslen(szTempDir);
-        for (int i = 0; i < 10000 && !fCreatedDirectory; i++)
+        hr = ::StringCchCat(szTempDir, cchTempDirBuf, szGuid);
+        if (FAILED(hr))
         {
-                swprintf_s(szTempDir + cchTempDir, cchTempDirBuf - cchTempDir, L"%d", i);
-                fCreatedDirectory = ::CreateDirectory(szTempDir, NULL);
+                Log(hSession, L"Failed append GUID to temp path. Error code 0x%x", hr);
+                goto LExit;
         }
 
-        if (!fCreatedDirectory)
+        if (!::CreateDirectory(szTempDir, NULL))
         {
-                Log(hSession, L"Failed to create temp directory. Error code %d", ::GetLastError());
-                return false;
+                hr = HRESULT_FROM_WIN32(::GetLastError());
+                Log(hSession, L"Failed to create temp directory. Error code 0x%x", hr);
+                goto LExit;
         }
 
         Log(hSession, L"Extracting custom action to temporary directory: %s\\", szTempDir);
         int err = ExtractCabinet(szModule, szTempDir);
         if (err != 0)
         {
+                hr = E_FAIL;
                 Log(hSession, L"Failed to extract to temporary directory. Cabinet error code %d.", err);
                 DeleteDirectory(szTempDir);
-                return false;
         }
-        return true;
-}
 
+LExit:
+        return SUCCEEDED(hr);
+}
diff --git a/src/dtf/dtf.cmd b/src/dtf/dtf.cmd
index d72c803b..530ee432 100644
--- a/src/dtf/dtf.cmd
+++ b/src/dtf/dtf.cmd
@@ -4,13 +4,36 @@
 @set _C=Debug
 :parse_args
 @if /i "%1"=="release" set _C=Release
+@if /i "%1"=="inc" set _INC=1
+@if /i "%1"=="clean" set _CLEAN=1
 @if not "%1"=="" shift & goto parse_args
 
+:: Clean
+
+@if "%_INC%"=="" call :clean
+@if NOT "%_CLEAN%"=="" goto :end
+
 @echo Building dtf %_C%
 
 msbuild -Restore SfxCA\sfxca_t.proj -p:Configuration=%_C% -tl -nologo -m -warnaserror -bl:..\..\build\logs\dtf_sfxca.binlog || exit /b
 
 msbuild -Restore -t:Pack dtf.sln -p:Configuration=%_C% -tl -nologo -m -warnaserror -bl:..\..\build\logs\dtf_build.binlog || exit /b
 
+@goto :end
+
+:clean
+@rd /s/q "..\..\build\dtf" 2> nul
+@del "..\..\build\artifacts\WixToolset.Dtf.*.nupkg" 2> nul
+@rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.compression" 2> nul
+@rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.compression.cab" 2> nul
+@rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.compression.zip" 2> nul
+@rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.customaction" 2> nul
+@rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.resources" 2> nul
+@rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.windowsinstaller" 2> nul
+@rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.windowsinstaller.linq" 2> nul
+@rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.windowsinstaller.package" 2> nul
+@exit /b
+
+:end
 @popd
 @endlocal
diff --git a/src/test/msi/TestData/CustomActionTests/ManagedCustomActions/Package.wxs b/src/test/msi/TestData/CustomActionTests/ManagedCustomActions/Package.wxs
index 9b1d224a..4159cd3e 100644
--- a/src/test/msi/TestData/CustomActionTests/ManagedCustomActions/Package.wxs
+++ b/src/test/msi/TestData/CustomActionTests/ManagedCustomActions/Package.wxs
@@ -13,13 +13,17 @@
     <Binary Id="ManagedCA" SourceFile="TestCA.CA.dll" />
 
     <CustomAction Id="ImmediateCA" BinaryRef="ManagedCA" DllEntry="ImmediateCA" Execute="immediate" Return="check" />
-    <CustomAction Id="DeferredCA" BinaryRef="ManagedCA" DllEntry="DeferredCA" Execute="deferred" Return="check" />
+    <CustomAction Id="ExecuteImmediateCA" BinaryRef="ManagedCA" DllEntry="ExecuteImmediateCA" Execute="immediate" Return="check" />
+    <CustomAction Id="ImpersonatedDeferredCA" BinaryRef="ManagedCA" DllEntry="ImpersonatedDeferredCA" Execute="deferred" Impersonate="yes" Return="check" />
+    <CustomAction Id="DeferredCA" BinaryRef="ManagedCA" DllEntry="DeferredCA" Execute="deferred" Impersonate="no" Return="check" />
 
     <InstallUISequence>
       <Custom Action="ImmediateCA" Before="CostFinalize" />
     </InstallUISequence>
 
     <InstallExecuteSequence>
+      <Custom Action="ExecuteImmediateCA" Before="CostFinalize" />
+      <Custom Action="ImpersonatedDeferredCA" Before="InstallFiles" />
       <Custom Action="DeferredCA" After="InstallFiles" />
     </InstallExecuteSequence>
   </Package>
diff --git a/src/test/msi/TestData/CustomActionTests/TestCA/CustomAction.cs b/src/test/msi/TestData/CustomActionTests/TestCA/CustomAction.cs
index 6891f8da..54afc2cf 100644
--- a/src/test/msi/TestData/CustomActionTests/TestCA/CustomAction.cs
+++ b/src/test/msi/TestData/CustomActionTests/TestCA/CustomAction.cs
@@ -4,7 +4,9 @@ namespace WixToolsetTest.TestCA
 {
     using System;
     using System.Collections.Generic;
+    using System.IO;
     using System.Linq;
+    using System.Text;
     using WixToolset.Dtf.WindowsInstaller;
 
     public class CustomActions
@@ -14,6 +16,31 @@ namespace WixToolsetTest.TestCA
         {
             session.Log("Begin ImmediateCA");
 
+            var path = Path.Combine(Path.GetTempPath(), "ImmediateCA.txt");
+            WriteNow(path);
+
+            return ActionResult.Success;
+        }
+
+        [CustomAction]
+        public static ActionResult ExecuteImmediateCA(Session session)
+        {
+            session.Log("Begin ExecuteImmediateCA");
+
+            var path = Path.Combine(Path.GetTempPath(), "ExecuteImmediateCA.txt");
+            WriteNow(path);
+
+            return ActionResult.Success;
+        }
+
+        [CustomAction]
+        public static ActionResult ImpersonatedDeferredCA(Session session)
+        {
+            session.Log("Begin ImpersonatedDeferredCA");
+
+            var path = Path.Combine(Path.GetTempPath(), "ImpersonatedDeferredCA.txt");
+            WriteNow(path);
+
             return ActionResult.Success;
         }
 
@@ -22,7 +49,18 @@ namespace WixToolsetTest.TestCA
         {
             session.Log("Begin DeferredCA");
 
+            WriteNow(@"C:\Windows\Installer\DeferredCA.txt");
+
             return ActionResult.Success;
         }
+
+        private static void WriteNow(string path)
+        {
+            using (var file = File.Create(path))
+            {
+                var now = Encoding.UTF8.GetBytes(DateTime.Now.ToString("o"));
+                file.Write(now, 0, now.Length);
+            }
+        }
     }
 }
-- 
cgit v1.2.3-55-g6feb