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>
This commit is contained in:
Drew MochakandManlyMarco authored and GitHub committed 2026-09-01 06:58:43 +02:00
1 parent 6d82b2e5fc
commit 7cb1246fcc
1 file changed
+36 -5
@@ -60,6 +60,26 @@ public class UnityChainloader : BaseChainloader<BaseUnityPlugin>
get => Application.unityVersion;
}
/// <summary>
/// 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.
/// </summary>
[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<BaseUnityPlugin>
{
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<BaseUnityPlugin>
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");