dotnet / sdk

Core functionality needed to create .NET Core projects, that is shared between Visual Studio and CLI
https://dot.net/core
MIT License
2.66k stars 1.06k forks source link

Application Manifest file not loading when having isolated COMReferences #42027

Open vruss opened 3 months ago

vruss commented 3 months ago

Description

Update: Created a minimal repro here: https://github.com/vruss/net8-app-manifest-bug You will see that the Framework build works fine but the NET8 build doesn't run fine.

I have run into an issue with a Framework 4.8 to NET 8 upgrade that has to do with the Application Manifest not loading or working when I have COMReferences in my .csproj file.

Without the COMReferences: image

With the COMReferences: image Notice that I have 2 Application Manifest files after adding the COMReferences.

I have done a text-diff on both Manifest files when building a Framework 4.8 and NET 8 build and they are identical. This leads me to believe there is something wrong with the runtime of NET 8 and not the SDK.

App.manifest:

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity type="win32" version="1.0.0.0" name="MyApplication.app"/>
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <!-- Windows 10 GUID -->
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
    </application>
  </compatibility>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</dependency>
</assembly>

Generated manifest:

<?xml version="1.0" encoding="utf-8"?>
<assembly xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd" manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" xmlns:co.v1="urn:schemas-microsoft-com:clickonce.v1" xmlns:co.v2="urn:schemas-microsoft-com:clickonce.v2">
  <assemblyIdentity name="Net8.WinForms.exe" version="1.0.0.0" type="win32" />
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <applicationRequestMinimum>
        <PermissionSet Unrestricted="true" ID="Custom" SameSite="site" />
        <defaultAssemblyRequest permissionSetReference="Custom" />
      </applicationRequestMinimum>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
  <file name="RxClientView.ocx" asmv2:size="10979304">
    <hash xmlns="urn:schemas-microsoft-com:asm.v2">
      <dsig:Transforms>
        <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
      </dsig:Transforms>
      <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
      <dsig:DigestValue>DwijLRVRyiWmJP4/F+2mTIJWDJ4=</dsig:DigestValue>
    </hash>
    <typelib tlbid="{e12dc790-3a8e-456b-afde-ca8f570e740b}" version="1.0" helpdir="C:\src\dev3\Dependencies\Rastrex\RxClientView.hlp" resourceid="0" flags="CONTROL,HASDISKIMAGE" />
    <comClass clsid="{86702dd4-6e0b-4d72-8715-c963f1ba38b3}" threadingModel="Apartment" tlbid="{e12dc790-3a8e-456b-afde-ca8f570e740b}" progid="RXCLIENTVIEW.RxClientViewCtrl.1" description="RxClientView Control" />
  </file>
  <file name="RxRedlines.dll" asmv2:size="1345592">
    <hash xmlns="urn:schemas-microsoft-com:asm.v2">
      <dsig:Transforms>
        <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
      </dsig:Transforms>
      <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
      <dsig:DigestValue>u/tvvN735Z/2J+OO3wvqEon2SOg=</dsig:DigestValue>
    </hash>
    <typelib tlbid="{db83c810-c405-4df2-8949-b40b5bb65b71}" version="1.0" helpdir="" resourceid="0" flags="HASDISKIMAGE" />
    <comClass clsid="{8710a9e5-c634-41b0-acd5-2f264b8fb50e}" threadingModel="Apartment" tlbid="{db83c810-c405-4df2-8949-b40b5bb65b71}" progid="RxRedlines.RxRed.1" description="RxRed Class" />
  </file>
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <!-- Windows 10 GUID -->
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
    </application>
  </compatibility>
</assembly>

Reproduction Steps

  1. Create 2 WinForms projects: one in Framework 4.8 and one in NET 8.
  2. Add an Application Manifest to both projects and set requireAdministrator to true. 2.1. Build and run and you will see the UAC prompt as expected.
  3. Add to the .csproj a COMReference. I am guessing it has to be Isolated. Just make sure that a second Application Manifest is generated for you when you build. 3.1. Build and run and you will see the UAC when running the Framework program but no UAC when running NET8.

The UAC is just an easy way for me to verify if the Application Manifest is loaded. I noticed this issue because my Win10 compatibility was not working.

Example of what my csprojs looks like:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWindowsForms>true</UseWindowsForms>
    <ImplicitUsings>enable</ImplicitUsings>
    <ApplicationManifest>app.manifest</ApplicationManifest>
  </PropertyGroup>

  <ItemGroup>
    <EmbeddedResource Include="app.manifest">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </EmbeddedResource>
  </ItemGroup>

  <ItemGroup>
    <COMReference Include="RXCLIENTVIEWLib">
      <Guid>{E12DC790-3A8E-456B-AFDE-CA8F570E740B}</Guid>
      <VersionMajor>1</VersionMajor>
      <VersionMinor>0</VersionMinor>
      <Lcid>0</Lcid>
      <WrapperTool>tlbimp</WrapperTool>
      <Isolated>True</Isolated>
      <EmbedInteropTypes>True</EmbedInteropTypes>
    </COMReference>
    <COMReference Include="RXREDLINESLib">
      <Guid>{DB83C810-C405-4DF2-8949-B40B5BB65B71}</Guid>
      <VersionMajor>1</VersionMajor>
      <VersionMinor>0</VersionMinor>
      <Lcid>0</Lcid>
      <WrapperTool>tlbimp</WrapperTool>
      <Isolated>True</Isolated>
      <EmbedInteropTypes>True</EmbedInteropTypes>
    </COMReference>
  </ItemGroup>

</Project>
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net48</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <ApplicationManifest>app.manifest</ApplicationManifest>
  </PropertyGroup>

  <ItemGroup>
    <EmbeddedResource Include="app.manifest">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </EmbeddedResource>
  </ItemGroup>

  <ItemGroup>
    <COMReference Include="RXCLIENTVIEWLib">
      <Guid>{E12DC790-3A8E-456B-AFDE-CA8F570E740B}</Guid>
      <VersionMajor>1</VersionMajor>
      <VersionMinor>0</VersionMinor>
      <Lcid>0</Lcid>
      <WrapperTool>tlbimp</WrapperTool>
      <Isolated>True</Isolated>
      <EmbedInteropTypes>True</EmbedInteropTypes>
    </COMReference>
    <COMReference Include="RXREDLINESLib">
      <Guid>{DB83C810-C405-4DF2-8949-B40B5BB65B71}</Guid>
      <VersionMajor>1</VersionMajor>
      <VersionMinor>0</VersionMinor>
      <Lcid>0</Lcid>
      <WrapperTool>tlbimp</WrapperTool>
      <Isolated>True</Isolated>
      <EmbedInteropTypes>True</EmbedInteropTypes>
    </COMReference>
  </ItemGroup>

</Project>

Expected behavior

I expect the Application Manifest to be respected and working in NET 8, the same way it did in Framework 4.8.

Actual behavior

The Application Manifest is ignored.

Regression?

Worked in Framework 4.8

Known Workarounds

Isolated false

Changing the COMReference import to be <Isolated>False</Isolated> fixed the issue for me.

Isolated true

  1. Download rcedit
  2. $ rcedit "path-to-exe" --application-manifest "./path/to/app.manifest"

Configuration

NET 8.0.204 Windows 10 x64 WinForms

Other information

No response

huoyaoyuan commented 3 months ago

What happens if you manually merge the content of generated manifest into app.manifest?

vruss commented 3 months ago

I copied and replaced the contents of the generated one into the manifest but it didn't work.

I actually found a workaround just now. In the .csproj file I just need to change Isolated to false on the COM reference. This makes it so that no manifest file is generated and the manifest file I created works.

This however might break the COM reference if it is required to be Isolated

vruss commented 2 months ago

Update: setting the COM reference to not be Isolated breaks my COM component. So now I have to choose between having a broken COM component and having a working manifest file.

AaronRobinsonMSFT commented 2 months ago

This is really an SDK problem.

vruss commented 1 month ago

@AaronRobinsonMSFT hey thank you for transferring this issue to the correct team!

This has become a critical issue for the company I work for. Is there anything we can do to speed up the process? Contribute ourselves, sponsor or bounty on it?

vruss commented 1 month ago

Looking at the executables we can see that if we have the COM references isolated as true then no Win32 resources are added to the executable. While the executable that had the COM refernces as isolated false has the Win32 resource.

Maybe this lack of Win32 resources is what is causing the isolated true to not load the manifest file?

image

vruss commented 1 month ago

Found another workaround / fix that works when running with <Isolated>True</Isolated>!

  1. Download rcedit
  2. $ rcedit "path-to-exe" --application-manifest "./path/to/app.manifest"

Running this program with that command add the missing Win32 resources to the <Isolated>True</Isolated> built executable resulting in this structure: image

And this is what the Win32 manifest looks like:

<?xml version="1.0" encoding="utf-8"?>
<assembly xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd" manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" xmlns:co.v1="urn:schemas-microsoft-com:clickonce.v1" xmlns:co.v2="urn:schemas-microsoft-com:clickonce.v2">
  <assemblyIdentity name="Net8.WinForms.exe" version="1.0.0.0" type="win32" />
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <applicationRequestMinimum>
        <PermissionSet Unrestricted="true" ID="Custom" SameSite="site" />
        <defaultAssemblyRequest permissionSetReference="Custom" />
      </applicationRequestMinimum>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
  <file name="RxClientView.ocx" asmv2:size="10979304">
    <hash xmlns="urn:schemas-microsoft-com:asm.v2">
      <dsig:Transforms>
        <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
      </dsig:Transforms>
      <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
      <dsig:DigestValue>DwijLRVRyiWmJP4/F+2mTIJWDJ4=</dsig:DigestValue>
    </hash>
    <typelib tlbid="{e12dc790-3a8e-456b-afde-ca8f570e740b}" version="1.0" helpdir="C:\src\dev3\Dependencies\Rastrex\RxClientView.hlp" resourceid="0" flags="CONTROL,HASDISKIMAGE" />
    <comClass clsid="{86702dd4-6e0b-4d72-8715-c963f1ba38b3}" threadingModel="Apartment" tlbid="{e12dc790-3a8e-456b-afde-ca8f570e740b}" progid="RXCLIENTVIEW.RxClientViewCtrl.1" description="RxClientView Control" />
  </file>
  <file name="RxRedlines.dll" asmv2:size="1345592">
    <hash xmlns="urn:schemas-microsoft-com:asm.v2">
      <dsig:Transforms>
        <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
      </dsig:Transforms>
      <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
      <dsig:DigestValue>u/tvvN735Z/2J+OO3wvqEon2SOg=</dsig:DigestValue>
    </hash>
    <typelib tlbid="{db83c810-c405-4df2-8949-b40b5bb65b71}" version="1.0" helpdir="" resourceid="0" flags="HASDISKIMAGE" />
    <comClass clsid="{8710a9e5-c634-41b0-acd5-2f264b8fb50e}" threadingModel="Apartment" tlbid="{db83c810-c405-4df2-8949-b40b5bb65b71}" progid="RxRedlines.RxRed.1" description="RxRed Class" />
  </file>
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <!-- Windows 10 GUID -->
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
    </application>
  </compatibility>
</assembly>
<!--Padding to make filesize even multiple of 4 XXXX -->
AaronRobinsonMSFT commented 1 month ago

Contribute ourselves, sponsor or bounty on it?

@vruss .NET is entirely open source. We welcome external contributions. Please reference the readme for details on this topic.

@marcpopMSFT Any chance we can get someone to look into this or provide some guidance on where @vruss can start?

baronfel commented 1 month ago

Where I'd start is getting a binlog of the build for each scenario - .NET framework 4.8 and modern .NET (you'll need to rename the file after each build to prevent clobbering) - and find the Targets and Tasks responsible for creating that manifest. Whichever component owns those tasks is the next group to ping - I don't know who that is off the top of my head, and without a more explicit repro since I'm unfamiliar with the space I can't help navigate who it might be.

vruss commented 1 month ago

@baronfel thanks for the guidance! I took a binlog but I don't really know what I'm looking for so I think that's as far as I can go with this one.

I made a minimal repro and uploaded it here: https://github.com/vruss/net8-app-manifest-bug You can easily reproduce the error by building and starting the executables. Your should see that the UAC is missing in the NET8 build.

baronfel commented 1 month ago

Thanks for the repo @vruss - I cloned it and did in fact notice the difference. The file is generated by the GenerateApplicationManifest Task, which is part of the MSbuild repo itself, but hasn't had any meaningful changes in quite some time.

The generated manifests themselves look very similar, so nothing jumps out to me as obviously wrong. @rainersigwald since we own this Task, do you have any idea who we'd poke about Windows manifests and how they apply to .NET executables?

rainersigwald commented 1 month ago

GenerateApplicationManifest is part of ClickOnce so let's try @sujitnayak.

rainersigwald commented 1 month ago

Ah wait, that's not the relevant manifest here though. This is the actual win32 manifest that needs to be embedded in the .exe file. Let's poke at that a bit.

rainersigwald commented 1 month ago

I don't have the repro right now--does it start working if you rename NET8.Console.dll.manifest to NET8.Console.exe.manifest?

baronfel commented 1 month ago

@rainersigwald in my clone of the project renaming the manifest did not make UAC start triggering.

rainersigwald commented 1 month ago

Ok we're somewhat out of my wheelhouse here but I think this is an SDK bug related to the differences in exe construction between .NET Framework and .NET apps.

For .NET Framework targets, the C# compiler itself emits a .exe file, which has embedded in it the manifest (as a "Resource"). That file is copied to the output directory and can be run, in which case (I think) Windows will scan for an embedded manifest before starting to run user code (and thus see and require elevation).

For .NET targets, the C# compiler emits a .dll file, which has the manifest embedded in it as a resource, and then creates a .exe that is a copy of the prototypal apphost.exe renamed to the application name. That is a small native application that loads the .NET 8 runtime and points it to the .dll that contains your application code.

What I think is happening here is that the win32 manifest should ideally be embedded into the apphost .exe so Windows can know to look for it before launching the application. That doesn't appear to be plumbed through in the SDK at the moment, and I think is the feature we'd need to make this scenario smooth.


A few things confuse me though:

  1. I can't use mt.exe -inputresource:D:\net8-app-manifest-bug\NET48.Console\bin\Debug\net4.8\NET48.Console.exe -out:extracted.manifest to see the embedded manifest of the net48 app. I thought that'd extract it the same way Windows does. Possibly a .NET Framework vs native win32 app behavior?
  2. It should be possible to have an external manifest (and indeed Csc is run with NoWin32Manifest = True). But that doesn't seem to work in either case (deleting the one next to the net48 app doesn't change anything and renaming the net8.0 one to match the .exe doesn't help).
  3. I think the apphost itself should have a win32 manifest (to avoid tripping Windows shim behavior, among other things). But it doesn't seem to from looking at hex dumps.
rainersigwald commented 1 month ago

If I do

> mt.exe -manifest NET8.Console.exe.manifest -outputresource:".\NET8.Console.exe;#1"
Microsoft (R) Manifest Tool
Copyright (c) Microsoft Corporation.
All rights reserved.

Then double-clicking on that app requests elevation. That feels like it confirms my hypothesis (but doesn't clear up any of my confusions).

baronfel commented 1 month ago

At this point this feels like a gap that the HostWriter class in the runtime should be handling for us, since we almost immediately delegate to that API in the CreateAppHost Task. @vitek-karas could we get your thoughts on this? Are we thinking in the right directions here?

sujitnayak commented 1 month ago

If COM Isolation is set to true, the manifest will not be embedded in the EXE. It needs be an external file for the regfree COM registration to work. Setting COM Isolation to false works b/c the manifest gets embedded in the .NET 8.0 EXE. For the case where Isolated is set to true, it looks like the external manifest is generated fine but it is not getting probed for some reason when the EXE launches.

vruss commented 1 month ago

When looking at the two executables inside DotPeek you can see that the .NET8 executable is:

  1. Missing metadata that is present inside the .dll
  2. Missing the Resources folder which contains the Manifest. You can see it inside deeper inside the NET8.Console

.NET Framework image

.NET 8 image

vitek-karas commented 1 month ago

@elinor-fung

elinor-fung commented 1 month ago

I think @sujitnayak's comment is the key here: https://github.com/dotnet/sdk/issues/42027#issuecomment-2270356380

If you look at the NET8.Console.dll that is produced when the COMReference has Isolated=true, it does not have have an embedded manifest - which means there will be no manifest in the resulting .exe, since HostWriter just copies the resources from the .dll to the .exe.

elinor-fung commented 1 month ago

I don't have the repro right now--does it start working if you rename NET8.Console.dll.manifest to NET8.Console.exe.manifest?

@rainersigwald in my clone of the project renaming the manifest did not make UAC start triggering.

I tried this and it did start triggering for me. But only if I hadn't already run it before renaming the manifest (not sure why - maybe Windows does some sort of caching).

KalleOlaviNiemitalo commented 1 month ago

Memories from Windows XP: procmon showed csrss.exe reading the manifest files so the cache was presumably in that process; and if the file write timestamp of the exe file had been changed, then it read the manifest file again instead of trusting cached data. Timestamps of the manifest file did not have the same effect.