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
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ public static partial class HostWriter
/// <param name="windowsGraphicalUserInterface">Specify whether to set the subsystem to GUI. Only valid for PE apphosts.</param>
/// <param name="assemblyToCopyResourcesFrom">Path to the intermediate assembly, used for copying resources to PE apphosts.</param>
/// <param name="enableMacOSCodeSign">Sign the app binary using codesign with an anonymous certificate.</param>
/// <param name="disableCetCompat">Remove CET Shadow Stack compatibility flag if set</param>
public static void CreateAppHost(
string appHostSourceFilePath,
string appHostDestinationFilePath,
string appBinaryFilePath,
bool windowsGraphicalUserInterface = false,
string assemblyToCopyResourcesFrom = null,
bool enableMacOSCodeSign = false)
bool enableMacOSCodeSign = false,
bool disableCetCompat = false)
{
var bytesToWrite = Encoding.UTF8.GetBytes(appBinaryFilePath);
if (bytesToWrite.Length > 1024)
Expand All @@ -48,7 +50,7 @@ public static void CreateAppHost(

bool appHostIsPEImage = false;

void RewriteAppHost(MemoryMappedViewAccessor accessor)
void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor accessor)
{
// Re-write the destination apphost with the proper contents.
BinaryUtils.SearchAndReplace(accessor, AppBinaryPathPlaceholderSearchValue, bytesToWrite);
Expand All @@ -64,6 +66,11 @@ void RewriteAppHost(MemoryMappedViewAccessor accessor)

PEUtils.SetWindowsGraphicalUserInterfaceBit(accessor);
}

if (disableCetCompat && appHostIsPEImage)
{
PEUtils.RemoveCetCompatBit(mappedFile, accessor);
}
}

try
Expand All @@ -85,7 +92,7 @@ void RewriteAppHost(MemoryMappedViewAccessor accessor)
long sourceAppHostLength = appHostSourceStream.Length;

// Transform the host file in-memory.
RewriteAppHost(memoryMappedViewAccessor);
RewriteAppHost(memoryMappedFile, memoryMappedViewAccessor);

// Save the transformed host.
using (FileStream fileStream = new FileStream(appHostDestinationFilePath, FileMode.Create))
Expand Down
33 changes: 33 additions & 0 deletions src/installer/managed/Microsoft.NET.HostModel/AppHost/PEUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,39 @@ public static bool IsPEImage(string filePath)
}
}

/// <summary>
/// Remove the CET Compat bit from extended DLL characteristics
/// </summary>
/// <param name="file">Memory-mapped PE file</param>
/// <param name="accessor"></param>
internal static void RemoveCetCompatBit(MemoryMappedFile file, MemoryMappedViewAccessor accessor)
{
using (PEReader reader = new PEReader(file.CreateViewStream(0, 0, MemoryMappedFileAccess.Read)))
{
// https://learn.microsoft.com/windows/win32/debug/pe-format#debug-type
const int IMAGE_DEBUG_TYPE_EX_DLLCHARACTERISTICS = 20;
foreach (DebugDirectoryEntry entry in reader.ReadDebugDirectory())
{
if ((int)entry.Type != IMAGE_DEBUG_TYPE_EX_DLLCHARACTERISTICS)
Comment thread
elinor-fung marked this conversation as resolved.
continue;

// Get the extended DLL characteristics from the debug directory entry data
ushort dllCharacteristics = AsLittleEndian(accessor.ReadUInt16(entry.DataPointer));

// Check for the CET compat bit
// https://learn.microsoft.com/windows/win32/debug/pe-format#extended-dll-characteristics
const ushort IMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT = 0x1;
if ((dllCharacteristics & IMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT) == 0)
break;

// Clear the CET compat bit
dllCharacteristics = (ushort)(dllCharacteristics & ~IMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT);
accessor.Write(entry.DataPointer, AsLittleEndian(dllCharacteristics));
Comment thread
elinor-fung marked this conversation as resolved.
break;
}
}
}

/// <summary>
/// This method will attempt to set the subsystem to GUI. The apphost file should be a windows PE file.
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,28 @@ private void RunApp(bool selfContained)
}
}

[ConditionalTheory(typeof(Binaries.CetCompat), nameof(Binaries.CetCompat.IsSupported))]
[InlineData(true)]
[InlineData(false)]
public void DisableCetCompat(bool selfContained)
{
SingleFileTestApp app = selfContained
? sharedTestState.SelfContainedApp.Copy()
: sharedTestState.FrameworkDependentApp.Copy();
app.CreateAppHost(disableCetCompat: true);

string singleFile = app.Bundle();
Command.Create(singleFile)
.CaptureStdErr()
.CaptureStdOut()
.DotNetRoot(TestContext.BuiltDotNet.BinPath, TestContext.BuildArchitecture)
.MultilevelLookup(false)
.Execute()
.Should().Pass()
.And.HaveStdOutContaining("Hello World")
.And.HaveStdOutContaining(TestContext.MicrosoftNETCoreAppVersion);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@

namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
{
public class PortableAppActivation : IClassFixture<PortableAppActivation.SharedTestState>
public class FrameworkDependentAppLaunch : IClassFixture<FrameworkDependentAppLaunch.SharedTestState>
{
private readonly SharedTestState sharedTestState;

public PortableAppActivation(SharedTestState fixture)
public FrameworkDependentAppLaunch(SharedTestState fixture)
{
sharedTestState = fixture;
}
Expand Down Expand Up @@ -90,9 +90,11 @@ public void Muxer_SpecificRuntimeConfig()
}

[Fact]
public void AppHost_FrameworkDependent_Succeeds()
public void AppHost()
{
string appExe = sharedTestState.App.AppExe;
if (Binaries.CetCompat.IsSupported)
Assert.True(Binaries.CetCompat.IsMarkedCompatible(appExe));

// Get the framework location that was built
string builtDotnet = TestContext.BuiltDotNet.BinPath;
Expand Down Expand Up @@ -125,7 +127,7 @@ public void AppHost_FrameworkDependent_Succeeds()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void AppHost_FrameworkDependent_GlobalLocation_Succeeds(bool useRegisteredLocation)
public void AppHost_GlobalLocation(bool useRegisteredLocation)
{
string appExe = sharedTestState.App.AppExe;

Expand Down Expand Up @@ -171,6 +173,24 @@ public void AppHost_FrameworkDependent_GlobalLocation_Succeeds(bool useRegistere
}
}

[ConditionalFact(typeof(Binaries.CetCompat), nameof(Binaries.CetCompat.IsSupported))]
public void AppHost_DisableCetCompat()
{
TestApp app = sharedTestState.App.Copy();
app.CreateAppHost(disableCetCompat: true);
Assert.False(Binaries.CetCompat.IsMarkedCompatible(app.AppExe));

Command.Create(app.AppExe)
.CaptureStdErr()
.CaptureStdOut()
.DotNetRoot(TestContext.BuiltDotNet.BinPath, TestContext.BuildArchitecture)
.MultilevelLookup(false)
.Execute()
.Should().Pass()
.And.HaveStdOutContaining("Hello World")
.And.HaveStdOutContaining(TestContext.MicrosoftNETCoreAppVersion);
}

[Fact]
public void RuntimeConfig_FilePath_Breaks_MAX_PATH_Threshold()
{
Expand Down Expand Up @@ -264,7 +284,7 @@ public void MissingFrameworkInRuntimeConfig_Fails(bool useAppHost)
[Theory]
[InlineData(true)]
[InlineData(false)]
public void AppHost_CLI_FrameworkDependent_MissingRuntimeFramework_ErrorReportedInStdErr(bool missingHostfxr)
public void AppHost_CLI_MissingRuntimeFramework_ErrorReportedInStdErr(bool missingHostfxr)
{
using (var invalidDotNet = TestArtifact.Create("cliErrors"))
{
Expand Down Expand Up @@ -306,7 +326,7 @@ public void AppHost_CLI_FrameworkDependent_MissingRuntimeFramework_ErrorReported

[Fact]
[PlatformSpecific(TestPlatforms.Windows)] // GUI app host is only supported on Windows.
public void AppHost_GUI_FrameworkDependent_MissingRuntimeFramework_ErrorReportedInDialog()
public void AppHost_GUI_MissingRuntimeFramework_ErrorReportedInDialog()
{
TestApp app = sharedTestState.App.Copy();
app.CreateAppHost(isWindowsGui: true);
Expand Down Expand Up @@ -407,7 +427,7 @@ public void AppHost_GUI_NoCustomErrorWriter_FrameworkMissing_ErrorReportedInDial

[Fact]
[PlatformSpecific(TestPlatforms.Windows)] // GUI app host is only supported on Windows.
public void AppHost_GUI_FrameworkDependent_DisabledGUIErrors_DialogNotShown()
public void AppHost_GUI_DisabledGUIErrors_DialogNotShown()
{
TestApp app = sharedTestState.App.Copy();
app.CreateAppHost(isWindowsGui: true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@

namespace HostActivation.Tests
{
public class StandaloneAppActivation : IClassFixture<StandaloneAppActivation.SharedTestState>
public class SelfContainedAppLaunch : IClassFixture<SelfContainedAppLaunch.SharedTestState>
{
private SharedTestState sharedTestState;

public StandaloneAppActivation(StandaloneAppActivation.SharedTestState fixture)
public SelfContainedAppLaunch(SelfContainedAppLaunch.SharedTestState fixture)
{
sharedTestState = fixture;
}
Expand All @@ -25,6 +25,9 @@ public StandaloneAppActivation(StandaloneAppActivation.SharedTestState fixture)
public void Default()
{
string appExe = sharedTestState.App.AppExe;
if (Binaries.CetCompat.IsSupported)
Assert.True(Binaries.CetCompat.IsMarkedCompatible(appExe));

Command.Create(appExe)
.CaptureStdErr()
.CaptureStdOut()
Expand All @@ -43,6 +46,22 @@ public void Default()
}
}

[ConditionalFact(typeof(Binaries.CetCompat), nameof(Binaries.CetCompat.IsSupported))]
public void AppHost_DisableCetCompat()
{
TestApp app = sharedTestState.App.Copy();
app.CreateAppHost(disableCetCompat: true);
Assert.False(Binaries.CetCompat.IsMarkedCompatible(app.AppExe));

Command.Create(app.AppExe)
.CaptureStdErr()
.CaptureStdOut()
.Execute()
.Should().Pass()
.And.HaveStdOutContaining("Hello World")
.And.HaveStdOutContaining(TestContext.MicrosoftNETCoreAppVersion);
}

[Fact]
public void NoDepsJson_NoRuntimeConfig()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Runtime.CompilerServices;
using System.Text;
Expand Down Expand Up @@ -110,9 +112,9 @@ public void GUISubsystem_WindowsPEFile()
windowsGraphicalUserInterface: true);

BitConverter
.ToUInt16(File.ReadAllBytes(destinationFilePath), SubsystemOffset)
.Should()
.Be((ushort)Subsystem.WindowsGui);
.ToUInt16(File.ReadAllBytes(destinationFilePath), SubsystemOffset)
.Should()
.Be((ushort)Subsystem.WindowsGui);

Assert.Equal((ushort)Subsystem.WindowsGui, PEUtils.GetWindowsGraphicalUserInterfaceBit(destinationFilePath));
}
Expand Down Expand Up @@ -304,6 +306,83 @@ public void CodeSigningFailuresThrow()
}
}

[Theory]
[InlineData(true)] // Bit is set in extended DLL characteristics
[InlineData(false)] // Bit is not set in extended DLL characteristics
[InlineData(null)] // No extended DLL characteristics
public void CetCompat(bool? cetCompatSet)
{
using (TestArtifact artifact = CreateTestDirectory())
{
// Create a PE image with with CET compatability enabled/disabled
BlobBuilder peBlob = Binaries.CetCompat.CreatePEImage(cetCompatSet);

// Add the placeholder - it just needs to exist somewhere in the image, as HostWriter.CreateAppHost requires it
peBlob.WriteBytes(AppBinaryPathPlaceholderSearchValue);

string source = Path.Combine(artifact.Location, "source.exe");
using (FileStream stream = new FileStream(source, FileMode.Create))
{
peBlob.WriteContentTo(stream);
}

bool originallyEnabled = cetCompatSet.HasValue ? cetCompatSet.Value : false;
Assert.Equal(originallyEnabled, Binaries.CetCompat.IsMarkedCompatible(source));

// Validate compatibility is disabled
string cetDisabled = Path.Combine(artifact.Location, "cetDisabled.exe");
HostWriter.CreateAppHost(
source,
cetDisabled,
"app",
disableCetCompat: true);
Assert.False(Binaries.CetCompat.IsMarkedCompatible(cetDisabled));

// Validate compatibility is not changed
string cetEnabled = Path.Combine(artifact.Location, "cetUnchanged.exe");
HostWriter.CreateAppHost(
source,
cetEnabled,
"app",
disableCetCompat: false);
Assert.Equal(originallyEnabled, Binaries.CetCompat.IsMarkedCompatible(cetEnabled));
}
}

[ConditionalFact(typeof(Binaries.CetCompat), nameof(Binaries.CetCompat.IsSupported))]
public void CetCompat_ProductHosts()
{
using (TestArtifact artifact = CreateTestDirectory())
{
string[] hosts = [Binaries.AppHost.FilePath, Binaries.SingleFileHost.FilePath];
foreach (string host in hosts)
{
// Hosts should be compatible with CET shadow stack by default
Assert.True(Binaries.CetCompat.IsMarkedCompatible(host));
string source = Path.Combine(artifact.Location, Path.GetFileName(host));
File.Copy(host, source);

// Validate compatibility is disabled
string cetDisabled = Path.Combine(artifact.Location, $"{Path.GetFileName(host)}_cetDisabled.exe");
HostWriter.CreateAppHost(
source,
cetDisabled,
"app",
disableCetCompat: true);
Assert.False(Binaries.CetCompat.IsMarkedCompatible(cetDisabled));

// Validate compatibility is not changed (remains enabled)
string cetEnabled = Path.Combine(artifact.Location, $"{Path.GetFileName(host)}_cetEnabled.exe");
HostWriter.CreateAppHost(
source,
cetEnabled,
"app",
disableCetCompat: false);
Assert.True(Binaries.CetCompat.IsMarkedCompatible(cetEnabled));
}
}
}

[Fact]
private void ResourceWithUnknownLanguage()
{
Expand Down
Loading