From 613c88f925357189f6cd0ef6cfa1630f2b91c4e6 Mon Sep 17 00:00:00 2001 From: w7rus Date: Wed, 15 Jul 2026 16:16:41 +0700 Subject: [PATCH] fix: Pin CallbackDelegate with GCHandle to prevent GC collection on .NET 10 Linux On .NET 10 Linux, Marshal.GetFunctionPointerForDelegate no longer internally pins the delegate, causing the GC to collect CallbackDelegate instances while native code still holds function pointers to them. This results in crashes with "callback was made on a garbage collected delegate". Add GCHandle.Alloc in FunctionReference constructor to explicitly prevent collection. Free the handle in Remove() during intentional cleanup. Also fix BasePlugin.Dispose() modifying dictionaries during foreach iteration by snapshotting values with .ToList() first. --- managed/CounterStrikeSharp.API/Core/BasePlugin.cs | 12 ++++++------ .../CounterStrikeSharp.API/Core/FunctionReference.cs | 7 +++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs index c90193071..41a0bb814 100644 --- a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs +++ b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs @@ -665,32 +665,32 @@ protected virtual void Dispose(bool disposing) if (_disposed) return; if (!disposing) return; - foreach (var subscriber in Handlers.Values) + foreach (var subscriber in Handlers.Values.ToList()) { subscriber.Dispose(); } - foreach (var subscriber in CommandListeners.Values) + foreach (var subscriber in CommandListeners.Values.ToList()) { subscriber.Dispose(); } - foreach (var subscriber in Listeners.Values) + foreach (var subscriber in Listeners.Values.ToList()) { subscriber.Dispose(); } - foreach (var subscriber in EntityOutputHooks.Values) + foreach (var subscriber in EntityOutputHooks.Values.ToList()) { subscriber.Dispose(); } - foreach (var definition in CommandDefinitions) + foreach (var definition in CommandDefinitions.ToList()) { CommandManager.RemoveCommand(definition); } - foreach (var timer in Timers) + foreach (var timer in Timers.ToList()) { timer.Kill(); } diff --git a/managed/CounterStrikeSharp.API/Core/FunctionReference.cs b/managed/CounterStrikeSharp.API/Core/FunctionReference.cs index 02bd8dd58..74794608a 100644 --- a/managed/CounterStrikeSharp.API/Core/FunctionReference.cs +++ b/managed/CounterStrikeSharp.API/Core/FunctionReference.cs @@ -49,6 +49,7 @@ public class FunctionReference private readonly Delegate _targetMethod; private readonly CallbackDelegate _nativeCallback; + private readonly GCHandle _nativeCallbackHandle; private readonly TaskCompletionSource _taskCompletionSource = new(); @@ -57,6 +58,7 @@ private FunctionReference(Delegate method, FunctionLifetime lifetime) Lifetime = lifetime; _targetMethod = method; _nativeCallback = CreateWrappedCallback(); + _nativeCallbackHandle = GCHandle.Alloc(_nativeCallback); } /// @@ -178,6 +180,11 @@ public static void Remove(int reference) IdToFunctionReferencesMap.Remove(reference, out _); + if (functionReference._nativeCallbackHandle.IsAllocated) + { + functionReference._nativeCallbackHandle.Free(); + } + Application.Instance.Logger.LogDebug("Removing function/callback reference: {Reference}", reference); } }