Fix: Server not recovering after a plugin crash (#7335)
What changed, and why it matters
This commit fixes a bug where BTCPay Server would not properly recover after a plugin crashed. The server tries to identify which plugin caused an exception by looking at the stack trace. The old code only checked the main assembly name and did not look inside nested/inner exceptions, so it sometimes failed to find the culprit plugin. The fix makes the search walk through inner exceptions and also maps all assemblies loaded by each plugin, not just the plugin's primary assembly. A second small fix prevents malformed plugin commands from crashing the command parser.
Review whether malformed plugin commands could previously trigger unhandled exceptions or command execution errors, and confirm the crash-recovery path is covered by tests. No immediate emergency action is indicated, but deploy as part of normal patching.
Security signals we found
Denial-of-service mitigation: improves server recovery after plugin crashes
Exception handling hardening: recursively inspects AggregateException and InnerException for plugin attribution
Input validation added to plugin command parsing (colon-split length check)
Plugin isolation boundary strengthened by tracking all assemblies in plugin load context
Evidence from the diff
PluginManager.cs is updated in two areas. First, ExtractPluginsFromStackTrace is renamed to ExtractPluginFromStackTrace and refactored into an overload pair. The public overload builds a Dictionary
Changed components
BTCPayServer/Plugins/PluginManager.csPlugin crash recovery logicPlugin command parsing from environment variablesInspect captured patch +37 / −5
diff --git a/BTCPayServer/Plugins/PluginManager.cs b/BTCPayServer/Plugins/PluginManager.cs
index 281e47c..8e18c25 100644
--- a/BTCPayServer/Plugins/PluginManager.cs
+++ b/BTCPayServer/Plugins/PluginManager.cs
@@ -37,7 +37,7 @@ namespace BTCPayServer.Plugins
public static bool IsExceptionByPlugin(Exception exception, [MaybeNullWhen(false)] out PreloadedPlugin preloadedPlugin)
{
- if (ExtractPluginsFromStackTrace(exception, out preloadedPlugin)) return true;
+ if (ExtractPluginFromStackTrace(exception, out preloadedPlugin)) return true;
var fromAssembly = exception is TypeLoadException
? Regex.Match(exception.Message, "from assembly '(.*?),").Groups[1].Value
@@ -72,9 +72,24 @@ namespace BTCPayServer.Plugins
return false;
}
- private static bool ExtractPluginsFromStackTrace(Exception exception, [MaybeNullWhen(false)] out PreloadedPlugin preloadedPlugin)
+ private static bool ExtractPluginFromStackTrace(Exception exception, [MaybeNullWhen(false)] out PreloadedPlugin preloadedPlugin)
+ {
+ Dictionary<string, PreloadedPlugin> pluginsByName = new();
+
+ foreach (var preloaded in _preloadedPlugins.Where(p => p.Loader is not null && !string.IsNullOrEmpty(p.Assembly.FullName)))
+ {
+ pluginsByName.TryAdd(preloaded.Assembly.FullName!, preloaded);
+ foreach (var assembly in preloaded.Loader!.LoadContext.Assemblies)
+ {
+ pluginsByName.TryAdd(assembly.FullName!, preloaded);
+ }
+ }
+ return ExtractPluginFromStackTrace(exception, out preloadedPlugin, pluginsByName);
+ }
+
+ private static bool ExtractPluginFromStackTrace(Exception exception, out PreloadedPlugin? preloadedPlugin,
+ Dictionary<string, PreloadedPlugin> pluginsByName)
{
- 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())
{
@@ -88,7 +103,20 @@ namespace BTCPayServer.Plugins
}
}
preloadedPlugin = null;
- return false;
+ if (exception is AggregateException aggregateException)
+ {
+ foreach (var ex in aggregateException.InnerExceptions)
+ {
+ if (ExtractPluginFromStackTrace(ex, out preloadedPlugin, pluginsByName))
+ return true;
+ }
+
+ return false;
+ }
+ else if (exception.InnerException is not null)
+ return ExtractPluginFromStackTrace(exception.InnerException, out preloadedPlugin, pluginsByName);
+ else
+ return false;
}
public record PreloadedPlugin(IBTCPayServerPlugin Instance, PluginLoader? Loader, Assembly Assembly);
@@ -496,8 +524,12 @@ namespace BTCPayServer.Plugins
return commands.Select(s =>
{
var split = s.Split(':');
+ if (split.Length != 2)
+ return (string.Empty, string.Empty);
return (split[0].ToLower(CultureInfo.InvariantCulture), split[1]);
- }).ToArray();
+ })
+ .Where(s => s.Item1 != "")
+ .ToArray();
}
public static void QueueCommands(string pluginsFolder, params (string action, string plugin)[] commands)
Why this scored 40/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.