What changed, and why it matters
This commit improves how BTCPay Server figures out whether a crash was caused by a third-party plugin. It adds a new check that inspects the crash's stack trace to see if any loaded plugin's code appears in it. This is a defensive reliability improvement, not a fix for an active security vulnerability. It helps the server better identify misbehaving plugins so they can be disabled or reported, reducing the chance that a bad plugin causes repeated outages or unexpected behavior.
Treat as a routine reliability/defensive improvement. Review whether the new stack-trace walk handles nested or async stack traces correctly, and ensure that plugin disabling logic downstream of IsExceptionByPlugin handles newly detected plugin crashes gracefully. No urgent security patch is indicated by this commit alone.
Security signals we found
Improves attribution of runtime exceptions to plugin assemblies
May reduce false negatives in plugin crash detection
No new attack surface introduced; read-only inspection of exception stack trace
Does not change sandboxing, permissions, or plugin loading model
Evidence from the diff
The change adds a new private method, ExtractPluginsFromStackTrace, to PluginManager.cs. It builds a dictionary of preloaded plugins keyed by Assembly.FullName, creates a StackTrace from the supplied Exception, and walks the frames in reverse order. For each frame it retrieves the method’s Module.Assembly.FullName and checks whether it matches a known plugin assembly. If so, it returns the plugin as the likely culprit. This method is now called first in IsExceptionByPlugin before the existing Source/TypeLoadException/referenced-assembly heuristics. The rest of the logic is unchanged except for minor comment edits.
Changed components
BTCPayServer/Plugins/PluginManager.csPlugin crash detection / exception attribution logicInspect captured patch +25 / −4
diff --git a/BTCPayServer/Plugins/PluginManager.cs b/BTCPayServer/Plugins/PluginManager.cs
index 2876853..310d000 100644
--- a/BTCPayServer/Plugins/PluginManager.cs
+++ b/BTCPayServer/Plugins/PluginManager.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
@@ -35,18 +36,19 @@ namespace BTCPayServer.Plugins
public static bool IsExceptionByPlugin(Exception exception, [MaybeNullWhen(false)] out PreloadedPlugin preloadedPlugin)
{
+ if (ExtractPluginsFromStackTrace(exception, out preloadedPlugin)) return true;
+
var fromAssembly = exception is TypeLoadException
? Regex.Match(exception.Message, "from assembly '(.*?),").Groups[1].Value
: null;
-
foreach (var plugin in _preloadedPlugins)
{
var assembly = plugin.Assembly;
var assemblyName = assembly.GetName().Name;
if (assemblyName is null)
continue;
- // Comparison is case sensitive as it is theoretically possible to have a different plugin
- // with same name but different casing.
+ // Comparison is case-sensitive as it is theoretically possible to have a different plugin
+ // with the same name but different casing.
if (exception.Source is not null &&
assemblyName.Equals(exception.Source, StringComparison.Ordinal))
{
@@ -58,7 +60,7 @@ namespace BTCPayServer.Plugins
preloadedPlugin = plugin;
return true;
}
- // For TypeLoadException, check if it might come from areferenced assembly
+ // For TypeLoadException, check if it might come from a referenced assembly
if (!string.IsNullOrEmpty(fromAssembly) && assembly.GetReferencedAssemblies().Select(a => a.Name).Contains(fromAssembly))
{
preloadedPlugin = plugin;
@@ -69,6 +71,25 @@ namespace BTCPayServer.Plugins
return false;
}
+ private static bool ExtractPluginsFromStackTrace(Exception exception, [MaybeNullWhen(false)] out PreloadedPlugin preloadedPlugin)
+ {
+ var pluginsByName = _preloadedPlugins.Where(p => p.Loader is not null).ToDictionary(p => p.Assembly.FullName ?? "", p => p);
+ var trace = new StackTrace(exception, true);
+ foreach (var frame in trace.GetFrames().Reverse())
+ {
+ var m = frame.GetMethod();
+ if (m is null)
+ continue;
+ if (pluginsByName.TryGetValue(m.Module.Assembly.FullName ?? "", out var plugin))
+ {
+ preloadedPlugin = plugin;
+ return true;
+ }
+ }
+ preloadedPlugin = null;
+ return false;
+ }
+
public record PreloadedPlugin(IBTCPayServerPlugin Instance, PluginLoader? Loader, Assembly Assembly);
class PreloadedPlugins : IEnumerable<PreloadedPlugin>
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.