From 7cb1246fcc05d680c454a51d2932c383c1e8843e Mon Sep 17 00:00:00 2001 From: Drew Mochak Date: Tue, 1 Sep 2026 00:58:43 -0400 Subject: [PATCH] Survive stripped UnityEngine.Application members during chainloader init (#1391) Two related failures on aggressively stripped Unity Mono builds: 1. InitializeLoggers reads Application.unityVersion, which the managed code stripper can remove. The version is only used for an informational log line, but a MissingMethodException there aborts the rest of chainloader initialisation. Read it defensively instead. 2. UnityChainloader.Initialize caught every exception, logged it as Fatal, and returned normally. StaticStart then called Execute() on a chainloader that was never initialised, so BepInEx logged "Chainloader startup complete" while every plugin silently never ran. Rethrow so a failed init cannot be mistaken for a successful one. Verified on Beacon Pines (Unity 2021.3.45f1, Mono, x64), which strips both Application.unityVersion and the log-callback members. Co-authored-by: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> --- .../Bootstrap/UnityChainloader.cs | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs b/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs index c08a8de..429ba56 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs @@ -60,6 +60,26 @@ public class UnityChainloader : BaseChainloader get => Application.unityVersion; } + /// + /// Reads the runtime Unity version, returning null if it is unavailable. + /// Unity's managed code stripper can remove Application.unityVersion entirely, in which case + /// touching it throws MissingMethodException. The version is only used for an informational + /// log line, so it must not be able to abort chainloader initialisation. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static string TryGetUnityVersion() + { + try + { + return UnityVersion; + } + catch (Exception e) + { + Logger.Log(LogLevel.Debug, $"Could not read the runtime Unity version: {e.Message}"); + return null; + } + } + [Obsolete("This method is public due to a limitation with Unity 4.x. DO NOT CALL", true)] public static void StaticStart(string gameExePath = null) { @@ -116,6 +136,12 @@ public class UnityChainloader : BaseChainloader { Logger.Log(LogLevel.Fatal, $"Unable to complete chainloader init: {ex.Message}"); Logger.Log(LogLevel.Fatal, ex.StackTrace); + + // Rethrow so callers do not go on to Execute() a chainloader that was never + // initialised. Swallowing here produces a far worse failure mode than crashing: + // the loader carries on to log "Chainloader startup complete" while every plugin + // silently never runs, which is extremely hard to diagnose. + throw; } } @@ -125,11 +151,16 @@ public class UnityChainloader : BaseChainloader Logger.Listeners.Add(new UnityLogListener()); - // Update version info from runtime in case it wasn't set yet - var prevVersion = UnityInfo.Version; - UnityInfo.SetRuntimeUnityVersion(UnityVersion); - if (UnityInfo.Version != prevVersion) - Logger.Log(LogLevel.Info, $"UnityPlayer version: {UnityInfo.Version}"); + // Update version info from runtime in case it wasn't set yet. + // May be unavailable on aggressively stripped builds; it is informational only. + var runtimeUnityVersion = TryGetUnityVersion(); + if (runtimeUnityVersion != null) + { + var prevVersion = UnityInfo.Version; + UnityInfo.SetRuntimeUnityVersion(runtimeUnityVersion); + if (UnityInfo.Version != prevVersion) + Logger.Log(LogLevel.Info, $"UnityPlayer version: {UnityInfo.Version}"); + } if (!ConfigDiskWriteUnityLog.Value) DiskLogListener.BlacklistedSources.Add("Unity Log");