diff --git a/docs/design/features/host-startup-hook.md b/docs/design/features/host-startup-hook.md index e2b4623fab038b..ec0e35eb5d16bb 100644 --- a/docs/design/features/host-startup-hook.md +++ b/docs/design/features/host-startup-hook.md @@ -253,3 +253,10 @@ be defined either in the app or within the first hook that uses it: The type should be made `internal` to prevent exposing it as API surface to any managed code that happens to have access to the startup hook dll. However, the feature will also work if the type is `public`. + +### Incompatible with trimming + +Startup hooks are disabled by default on trimmed apps. The usage of +startup hooks on a trimmed app is potentially dangerous since these +could make use of assemblies, types or members that were removed by +trimming, causing the app to crash. diff --git a/docs/workflow/trimming/feature-switches.md b/docs/workflow/trimming/feature-switches.md index 40d3a2299f474b..b0dadfd38cdf77 100644 --- a/docs/workflow/trimming/feature-switches.md +++ b/docs/workflow/trimming/feature-switches.md @@ -15,6 +15,7 @@ configurations but their defaults might vary as any SDK can set the defaults dif | InvariantGlobalization | System.Globalization.Invariant | All globalization specific code and data is trimmed when set to true | | UseSystemResourceKeys | System.Resources.UseSystemResourceKeys | Any localizable resources for system assemblies is trimmed when set to true | | HttpActivityPropagationSupport | System.Net.Http.EnableActivityPropagation | Any dependency related to diagnostics support for System.Net.Http is trimmed when set to false | +| StartupHookSupport | System.StartupHookProvider.IsSupported | Startup hooks are disabled when set to false. Startup hook related functionality can be trimmed. | Any feature-switch which defines property can be set in csproj file or on the command line as any other MSBuild property. Those without predefined property name diff --git a/src/coreclr/src/System.Private.CoreLib/src/System/StartupHookProvider.cs b/src/coreclr/src/System.Private.CoreLib/src/System/StartupHookProvider.cs index ffdf388e43e335..1ad92333498e0a 100644 --- a/src/coreclr/src/System.Private.CoreLib/src/System/StartupHookProvider.cs +++ b/src/coreclr/src/System.Private.CoreLib/src/System/StartupHookProvider.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Runtime.Loader; @@ -14,6 +16,8 @@ internal static class StartupHookProvider private const string InitializeMethodName = "Initialize"; private const string DisallowedSimpleAssemblyNameSuffix = ".dll"; + private static bool IsSupported => AppContext.TryGetSwitch("System.StartupHookProvider.IsSupported", out bool isSupported) ? isSupported : true; + private struct StartupHookNameOrPath { public AssemblyName AssemblyName; @@ -24,6 +28,9 @@ private struct StartupHookNameOrPath // containing a startup hook, and call each hook in turn. private static void ProcessStartupHooks() { + if (!IsSupported) + return; + // Initialize tracing before any user code can be called. System.Diagnostics.Tracing.RuntimeEventSource.Initialize(); @@ -96,6 +103,8 @@ private static void ProcessStartupHooks() // Load the specified assembly, and call the specified type's // "static void Initialize()" method. + [RequiresUnreferencedCode("The StartupHookSupport feature switch has been enabled for this app which is being trimmed. " + + "Startup hook code is not observable by the trimmer and so required assemblies, types and members may be removed")] private static void CallStartupHook(StartupHookNameOrPath startupHook) { Assembly assembly; diff --git a/src/installer/tests/HostActivation.Tests/RuntimeConfig.cs b/src/installer/tests/HostActivation.Tests/RuntimeConfig.cs index 6e810dc5d7cde8..eaa95cb2dcced0 100644 --- a/src/installer/tests/HostActivation.Tests/RuntimeConfig.cs +++ b/src/installer/tests/HostActivation.Tests/RuntimeConfig.cs @@ -276,7 +276,9 @@ public void Save() JObject configProperties = new JObject(); foreach (var property in _properties) { - configProperties.Add(property.Item1, property.Item2); + var tokenValue = (property.Item2 == "false" || property.Item2 == "true") ? + JToken.Parse(property.Item2) : property.Item2; + configProperties.Add(property.Item1, tokenValue); } runtimeOptions.Add("configProperties", configProperties); diff --git a/src/installer/tests/HostActivation.Tests/StartupHooks.cs b/src/installer/tests/HostActivation.Tests/StartupHooks.cs index 12d9e23fc84a2c..3acb8b6656506c 100644 --- a/src/installer/tests/HostActivation.Tests/StartupHooks.cs +++ b/src/installer/tests/HostActivation.Tests/StartupHooks.cs @@ -13,6 +13,7 @@ public class StartupHooks : IClassFixture { private SharedTestState sharedTestState; private string startupHookVarName = "DOTNET_STARTUP_HOOKS"; + private string startupHookSupport = "System.StartupHookProvider.IsSupported"; public StartupHooks(StartupHooks.SharedTestState fixture) { @@ -639,6 +640,32 @@ public void Muxer_activation_of_StartupHook_With_Assembly_Resolver() .And.ExitWith(2); } + [Fact] + public void Muxer_activation_of_StartupHook_With_IsSupported_False() + { + var fixture = sharedTestState.PortableAppFixture.Copy(); + var dotnet = fixture.BuiltDotnet; + var appDll = fixture.TestProject.AppDll; + + var startupHookFixture = sharedTestState.StartupHookFixture.Copy(); + var startupHookDll = startupHookFixture.TestProject.AppDll; + + RuntimeConfig.FromFile(fixture.TestProject.RuntimeConfigJson) + .WithProperty(startupHookSupport, "false") + .Save(); + + // Startup hooks are not executed when the StartupHookSupport + // feature switch is set to false. + dotnet.Exec(appDll) + .EnvironmentVariable(startupHookVarName, startupHookDll) + .CaptureStdOut() + .CaptureStdErr() + .Execute() + .Should().Pass() + .And.NotHaveStdOutContaining("Hello from startup hook!") + .And.HaveStdOutContaining("Hello World"); + } + public class SharedTestState : IDisposable { // Entry point projects diff --git a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml index 9831ce9b6e2d2b..3b2cd77a1a5d1b 100644 --- a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml +++ b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml @@ -18,5 +18,8 @@ + + + diff --git a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Suppressions.Shared.xml b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Suppressions.Shared.xml index dda85d233600a9..f13541318ac62b 100644 --- a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Suppressions.Shared.xml +++ b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Suppressions.Shared.xml @@ -83,7 +83,7 @@ ILLink IL2026 member - M:System.StartupHookProvider.CallStartupHook(System.StartupHookProvider.StartupHookNameOrPath) + M:System.StartupHookProvider.ProcessStartupHooks() ILLink