Closed julien-lebot closed 10 months ago
New debugging info.
This happens during CreateFolder
rollback. Here's a callstack from procmon.
You can reproduce clearing the AI
flag with python
import win32security
sd=win32security.GetFileSecurity("C:\\", win32security.DACL_SECURITY_INFORMATION)
win32security.SetFileSecurity("C:\\", win32security.DACL_SECURITY_INFORMATION, sd)
I think that the WiX# AutoElements
code is generating elements that cause C:\\
and other directories to be put in the MSIs CreateFolder
table. Then any directories in that table are subject to this Windows Installer bug.
WiX# shouldn't be generating Component
/CreateFolder
/RemoveFolder
elements for Directories that are outside of the installation, right?
Txs for reporting. Currently investigating it...
WiX# shouldn't be generating Component/CreateFolder/RemoveFolder elements for Directories that are outside of the installation, right?
Yes and it seems like it does not. This is the unmodified WXS content of your sample:
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="6f330b47-2577-43ad-9095-1861ca25889c" Name="MyProduct" Language="1033" Codepage="Windows-1252" Version="1.0.0.0" UpgradeCode="6f330b47-2577-43ad-9095-1861ba25889b" Manufacturer="oleg.shilo">
<Package InstallerVersion="200" Compressed="yes" SummaryCodepage="Windows-1252" Languages="1033" />
<Media Id="1" Cabinet="MyProduct.cab" EmbedCab="yes" />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder" Name="ProgramFilesFolder">
<Directory Id="My20Company" Name="My Company">
<Directory Id="INSTALLDIR" Name="My Product">
<Component Id="Component.readme.txt_1240382565" Guid="6f330b47-2577-43ad-9095-18615005cac4">
<File Id="readme.txt_1240382565" Source="readme.txt" />
</Component>
<Component Id="RemoveRegistryKey" Guid="6f330b47-2577-43ad-9095-18615e65d734" KeyPath="yes">
<RemoveRegistryKey Id="RemoveRegistryKey" Root="HKLM" Key="Software\My Company\My Product" Action="removeOnUninstall" />
<CreateFolder />
<RemoveFolder Id="INSTALLDIR" On="uninstall" />
</Component>
</Directory>
<Component Id="My20Company" Guid="6f330b47-2577-43ad-9095-186154b4b41d" KeyPath="yes">
<RemoveFolder Id="My20Company" On="uninstall" />
</Component>
</Directory>
<Component Id="Registry.1" Guid="6f330b47-2577-43ad-9095-1861b742561a" KeyPath="yes">
<RegistryKey Root="HKLM" Key="Software\My Company\My Product">
<RegistryValue Id="LICENSE_KEY" Type="binary" KeyPath="no" Value="01020304" Name="LICENSE_KEY">
<Permission Read="yes" User="usr" />
</RegistryValue>
</RegistryKey>
<CreateFolder />
<RemoveFolder Id="ProgramFilesFolder" On="uninstall" />
</Component>
<Component Id="Registry.2" Guid="6f330b47-2577-43ad-9095-1861b743561a">
<RegistryKey Root="HKLM" Key="Software\My Company\My Product">
<RegistryValue Id="Message" Type="string" KeyPath="yes" Value="Hello" Name="Message" />
</RegistryKey>
<CreateFolder />
</Component>
<Component Id="Registry.3" Guid="6f330b47-2577-43ad-9095-1861b744561a">
<RegistryKey Root="HKLM" Key="Software\My Company\My Product">
<RegistryValue Id="Count" Type="integer" KeyPath="yes" Value="777" Name="Count" />
</RegistryKey>
<CreateFolder />
</Component>
<Component Id="Registry.4" Guid="6f330b47-2577-43ad-9095-1861b73d561a">
<RegistryKey Root="HKLM" Key="Software\My Company\My Product">
<RegistryValue Id="Index" Type="integer" KeyPath="yes" Value="333" Name="Index" />
</RegistryKey>
<CreateFolder />
</Component>
<Component Id="Registry.5" Guid="6f330b47-2577-43ad-9095-1861b73e561a">
<RegistryKey Root="HKCR" Key="test\shell\open\command">
<RegistryValue Id="RegValue" Type="expandable" KeyPath="yes" Value=""[INSTALLDIR]test.exe" "%1"" />
</RegistryKey>
<CreateFolder />
</Component>
<Component Id="Registry.6" Guid="6f330b47-2577-43ad-9095-1861b73f561a">
<RegistryKey Root="HKCR" Key="test\shell\open\command2">
<RegistryValue Id="RegValue.1" Type="expandable" KeyPath="yes" Value=""[CommonAppDataFolder]test.exe" "%1"" />
</RegistryKey>
<CreateFolder />
</Component>
</Directory>
<Component Id="TARGETDIR" Guid="6f330b47-2577-43ad-9095-18612df5f80e" KeyPath="yes">
<RemoveFolder Id="TARGETDIR" On="uninstall" />
</Component>
</Directory>
<Feature Id="MyApp20Binaries" Title="MyApp Binaries" Absent="allow" Level="1">
<ComponentRef Id="Component.readme.txt_1240382565" />
<ComponentRef Id="Registry.1" />
<ComponentRef Id="Registry.2" />
<ComponentRef Id="Registry.3" />
<ComponentRef Id="Registry.4" />
<ComponentRef Id="Registry.5" />
<ComponentRef Id="Registry.6" />
<ComponentRef Id="RemoveRegistryKey" />
<ComponentRef Id="My20Company" />
<ComponentRef Id="TARGETDIR" />
</Feature>
<CustomActionRef Id="WixFailWhenDeferred" />
</Product>
</Wix>
Note, the solution creates TARGETDIR\ProgramFilesFolder\My Company\My Product
dir.
The CreateFolder
element is auto-inserted only for ProgramFilesFolder
, My Company
and My Product
.
Thus I am not sure even why C:\
is in the picture at all.
There are questionable RemoveFolder
in TARGETDIR
and in ProgramFilesFolder
but they are only exercised on uninstall.
These dirs will not be removed at runtime anyway as they are always non-empty.
I am completely puzzled.
BTW this is the routine that inserts CreateFolder for any component of the Dir without files: https://github.com/oleg-shilo/wixsharp/blob/7601893a5e9e14408ce9ef63415290d5c56254a6/Source/src/WixSharp/AutoElements.cs#L655C28-L655C28.
I can only suggest that you try removing CreateFolder/RemoveFolder and see if it impacts the behaviour:
project.WixSourceGenerated += (doc) =>
{
doc.FindAll("CreateFolder")
.ForEach(x => x.Remove());
doc.FindAll("RemoveFolder")
.ForEach(x => x.Remove());
};
Thanks for your input Oleg.
Thus I am not sure even why C:\ is in the picture at all.
If you look at the call stack Branden provided, I believe it could be the roll back action of CreateFolder
on TARGETDIR
.
I can only suggest that you try removing CreateFolder/RemoveFolder and see if it impacts the behaviour:
Yes, removing the entries in the CreateFolder
table as a post-processing step is our approach as well.
Wix# also seems to be auto-generating components for the TARGETDIR
, ProgramFiles64Folder
and the installation root which end up creating entries in the CreateFolder
table, so we had to remove them as well, see https://github.com/DataDog/datadog-agent/pull/19298/files#diff-c15f6fd4e6dba7d135a55a39a62d22db6386a879bf0d11c4dde2edf2d1426a13R253-R266
Interesting.
If you look at the call stack Branden provided, I believe it could be the roll back action of CreateFolder on TARGETDIR.
Actually, the wxs I provided does not have CreateFolder
on TARGETDIR
.
Wix# also seems to be auto-generating components for the TARGETDIR, ProgramFiles64Folder and the installation root which end up creating entries in the CreateFolder table.
Let's bring it all together
dir | component | CreateFolder | RemoveFolder |
---|---|---|---|
TARGETDIR | yes | no | yes |
ProgramFiles64Folder | yes | yes | yes |
<root> | yes | yes | yes |
TARGETDIR
TARGETDIR
is not the suspect. Interestingly enough I tried to suspend the generation of the component element for TARGETDIR but it triggered the compile error error LGHT0094 : Unresolved reference to symbol 'Component:TARGETDIR'
.
There is a small chance that it is RemoveFolder
folder that triggers it indirectly. This is easy to test by simply removing it before the compilation.
Interestingly enough WiX4 completely ditched TARGETDIR
concept.
ProgramFiles64Folder
I argue that ProgramFiles64Folder
as any other standard dir should have CreateFolder
as it can be some standard dir that does not exist on the target system during the installation. Though it's debatable.
<root>
This is the case when the CreateFolder
has all the reasons to exist.
Thus I am still not sure why C:\
is impacted.
BTW, you can always suppress the auto-generation of the CreateFolder
element:
AutoElements.SupportEmptyDirectories = CompilerSupportState.Disabled;
Actually, the wxs I provided does not have CreateFolder on TARGETDIR. Thus I am still not sure why C:\ is impacted.
I've been using Orca to examine the CreateFolder
MSI table. The CreateFolder
element is not the only thing that causes folders to be added to the CreateFolder
MSI table. We also have to remove the Component
elements that are created to host the CreateFolder
and RemoveFolder
elements for each directory. It seems that Component
elements with KeyPath="yes"
set also add the parent Directory
element to the CreateFolder
MSI table, regardless of if there is a CreateFolder
, RemoveFolder
, or any other element inside that component.
TARGETDIR is not the suspect. Interestingly enough I tried to suspend the generation of the component element for TARGETDIR but it triggered the compile error error LGHT0094 : Unresolved reference to symbol 'Component:TARGETDIR'.
There is a ComponentRef
element under the Feature
element that must also be removed.
BTW, you can always suppress the auto-generation of the CreateFolder element:
We tried this and it does remove all of the CreateFolder
elements, but there's still something else that is auto-generating a Component
for TARGETDIR
along with a RemoveFolder
. This also puts TARGETDIR
in the CreateFolder
MSI table.
I argue that ProgramFiles64Folder as any other standard dir should have CreateFolder as it can be some standard dir that does not exist on the target system during the installation.
<root>
: This is the case when the CreateFolder has all the reasons to exist.
Won't the Windows Installer recursively create the directory tree necessary to place files that it is installing, without needing an explicit entry in the CreateFolder
table? I thought it was only necessary for creating empty directories.
Won't the Windows Installer recursively create the directory tree necessary to place files that it is installing, without needing an explicit entry in the CreateFolder table? I thought it was only necessary for creating empty directories.
No, if the folders are empty (no files). And support for empty folders is a feature that was requested and later added to WixSharp.
...There is a ComponentRef element under the Feature element that must also be removed. ...but there's still something else that is auto-generating a Component for TARGETDIR along with a RemoveFolder.
Then it explains. IMHO the concept of component is fundamentally flawed. Instead of being just a group of deployment artifacts, it plays the role of the triggers for certain runtime behaviour (e.g. creating a folder). Then "CreatFolder" instead of encoding only folder creation triggers changes in the security context of the file system elements. WiX is full of these antipatterns. And WiXShar is an attempt to at least minimize the diversity of the combinations of these antipatterns.
Anyway, I would prefer to implement the countermeasures so let me have a look at the TARGETDIR
business. I think I can clean it and have the problem addressed. Basically what you are reporting is a new (previously unknown) side-effect of WixSharp auto-generation. So the auto-generation algorithm needs to be reviewed.
I have found the trigger for creating entries in the CreateFolder table when WXS does not ask for it (that Registry sample).
It is the RemoveRegistryKey
element. In contrast to RegistryKey
, it requires the corresponding entry of its parent dir in the CreateFolder
. This is achieved via KeyPath
attribute:
LGHT0204 : ICE18: KeyPath for Component: 'RemoveRegistryValue' is Directory:
'ProgramFilesFolder'. The Directory/Component pair must be listed in the CreateFolders table.
However if you remove 'RemoveRegistryValue' from the sample it will not even create a CreateFolder
table.
AutoElements.SupportEmptyDirectories = CompilerSupportState.Disabled;
var project =
new Project("MyProduct",
new Dir(@"%ProgramFiles%\My Company\My Product",
new File(fullSetup, @"readme.txt")),
new RegValue(fullSetup, RegistryHive.LocalMachine, @"Software\My Company\My Product", "LICENSE_KEY", "01020304") { AttributesDefinition = "Type=binary", Permissions = new[] { new Permission { User = "usr", Read = true } } },
new RegValue(fullSetup, RegistryHive.ClassesRoot, @"test\shell\open\command", "", "\"[INSTALLDIR]test.exe\" \"%1\""),
new RegValue(fullSetup, RegistryHive.ClassesRoot, @"test\shell\open\command2", "", "\"[CommonAppDataFolder]test.exe\" \"%1\""),
The remaining question is how to avoid modifying C:\ drive attributes if you have to use RemoveRegistryValue
.
I found that the presence of TARGETDIR in CreateFolder table seems to have no impact so I can easily remove it during the build.
Can you please help me to test the theory that the C:\
problem is caused by a very specific entry TARGETDIR in CreateTable, not any other dir.
Can you please rebuild your msi with this modification and then test in your environment?
project.WixSourceGenerated += (doc) =>
{
doc.FindAll("ComponentRef")
.Where(x => x.HasAttribute("Id", "TARGETDIR"))
.ForEach(x => x.Remove());
doc.FindAll("Component")
.Where(x => x.HasAttribute("Id", "TARGETDIR"))
.ForEach(x => x.Remove());
};
Note, we are trying to find a workaround but not a solution. The solution is for the CMsiOpExecute::CreateFolder
to stop messing with the drive attributes. It is obvious, we have no control over it.
Interesting reading about KeyPath
here: https://stackoverflow.com/questions/2003043/what-is-the-wix-keypath-attribute
It again confirms that WiX is just a more manageable interface to a barely manageable MSI db schema. And if one wants to be effective he/she still often needs to think about deployment not as an algorithm but a collection of the MSI tables 😞
It looks like there is no further info is available from the issue author. So closing it for now.
I have a short and disappointing update.
We contacted Microsoft Support (on 09/2023) and they confirmed that it is indeed a bug in MSI.DLL. At first they said they were going to fix it (on 02/2024) but they later changed their mind (on 06/2024). They need additional business and impact justification in order to prioritize it.
The underlying root cause is that MSI.DLL uses the obsolete SetFileSecurityW
API, as described in https://github.com/oleg-shilo/wixsharp/issues/1336#issuecomment-1703610582, instead of the newer SetNamedSecurityInfoW
API which supports ACE inheritance.
So all we can do is implement the workarounds described above to try to avoid the scenarios in which MSI.DLL will call SetFileSecurityW
. It's not as much of an issue if it only affects folders that are going to be removed on rollback, or if your installer also modifies the permissions of those folders on install. But it becomes a larger issue if system folders like C:\
or parent folders that linger or are shared with other MSIs make their way into the CreateFolder
table or become KeyPaths
, and thus become a target of SetFileSecurityW
during rollback.
If anyone comes across this issue in the future and contacts Microsoft about it, you can reference our support case number 2309110010002832.
They need additional business and impact justification in order to prioritize it.
Would not a lurking security hole and high chances for exploitation, when otherwise well protected and unsuspected installed product, can be exploited accidently or on purpose, is a very significant “business and impact justification”, considering that it is a Microsoft bug? Investigation will link it to the bug Microsoft knew about but decided to ignore due to the lack of “business justification”?
@clarkb7, Brandon, thank you so much for sharing such useful detailed feedback. Much appreciated
@oleg-shilo thank you for your in depth assistance!
Bug description
When the installer rolls back SE_DACL_AUTO_INHERITED is removed from the
C:\
drive.Steps to reproduce
Start from the Registry sample code. Add the following bit of code to import the
WIXFAILWHENDEFERRED
custom action:Check the ACLs on the
C:\
drive:Note the
AI
flag for the auto inherited DACL.Run the installer as follows:
Check the ACLs on the
C:\
drive:The
AI
flag was removed.