Disable all plugins when BTCPay Server crash during startup (#7046)
What changed, and why it matters
This commit changes how BTCPay Server handles crashes caused by plugins during startup. Previously, if a plugin caused a crash, the server would try to disable just that one plugin. The new code can disable all plugins when the server cannot tell which specific plugin is at fault, and it also avoids disabling built-in 'system' plugins. This is a reliability and recovery improvement rather than a fix for a remote attack.
Treat as a defensive hardening patch. Review whether disabling all user plugins on ambiguous startup crashes could itself be used as a denial-of-service vector (a malicious or buggy plugin could force all other plugins offline), and consider whether administrators should be prompted before mass-disabling. No immediate exploit mitigation is evident.
Security signals we found
Crash recovery / fault isolation hardening
Prevents accidental disabling of system plugins
Adds broad disable-all-plugins fallback on ambiguous startup failures
Evidence from the diff
The patch refactors plugin tracking from a simple list of assemblies to a PreloadedPlugins collection carrying plugin metadata (Instance, Loader, Assembly). It updates IsExceptionByPlugin to return the full PreloadedPlugin record, adds guards so SystemPlugin instances are never disabled, and introduces DisablePlugins to disable every non-system plugin. In Program.cs, startup crash handling now distinguishes a special case where a system plugin is blamed and the exception source is Microsoft.Extensions.DependencyInjection; in that case it disables all plugins because the offending plugin could not be pinpointed. Otherwise it disables the specific plugin identified.
Changed components
BTCPayServer/Plugins/PluginExceptionHandler.csBTCPayServer/Plugins/PluginManager.csBTCPayServer/Program.csInspect captured patch +46 / −17
diff --git a/BTCPayServer/Plugins/PluginExceptionHandler.cs b/BTCPayServer/Plugins/PluginExceptionHandler.cs
index 734f995..5aafd05 100644
--- a/BTCPayServer/Plugins/PluginExceptionHandler.cs
+++ b/BTCPayServer/Plugins/PluginExceptionHandler.cs
@@ -31,16 +31,17 @@ namespace BTCPayServer.Plugins
public ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
if (!GetDisablePluginIfCrash(httpContext) ||
- !PluginManager.IsExceptionByPlugin(exception, out var pluginName))
+ !PluginManager.IsExceptionByPlugin(exception, out var plugin) ||
+ plugin.Instance.SystemPlugin)
return ValueTask.FromResult(false);
- _logs.Configuration.LogError(exception, $"Unhandled exception caused by plugin '{pluginName}', disabling it and restarting...");
+ _logs.Configuration.LogError(exception, $"Unhandled exception caused by plugin '{plugin.Assembly.GetName().Name}', disabling it and restarting...");
if (Debugger.IsAttached)
{
_logs.Configuration.LogWarning("Debugger attached detected, so we didn't disable the plugin and do not restart the server");
return ValueTask.FromResult(false);
}
- PluginManager.DisablePlugin(_pluginDir, pluginName);
+ PluginManager.DisablePlugin(_pluginDir, plugin);
_ = Task.Delay(3000).ContinueWith((t) => _applicationLifetime.StopApplication());
// Returning true here means we will see Error 500 error message.
// Returning false means that the user will see a stacktrace.
diff --git a/BTCPayServer/Plugins/PluginManager.cs b/BTCPayServer/Plugins/PluginManager.cs
index 272e4ed..5c2ba9b 100644
--- a/BTCPayServer/Plugins/PluginManager.cs
+++ b/BTCPayServer/Plugins/PluginManager.cs
@@ -26,16 +26,21 @@ namespace BTCPayServer.Plugins
public static class PluginManager
{
public const string BTCPayPluginSuffix = ".btcpay";
- private static readonly List<Assembly> _pluginAssemblies = new ();
+ /// <summary>
+ /// In case of tests, this is shared the plugins that are already their assembly loaded.
+ /// This avoid loading the same plugin twice.
+ /// </summary>
+ private static PreloadedPlugins _preloadedPlugins = new();
- public static bool IsExceptionByPlugin(Exception exception, [MaybeNullWhen(false)] out string pluginName)
+ public static bool IsExceptionByPlugin(Exception exception, [MaybeNullWhen(false)] out PreloadedPlugin preloadedPlugin)
{
var fromAssembly = exception is TypeLoadException
? Regex.Match(exception.Message, "from assembly '(.*?),").Groups[1].Value
: null;
- foreach (var assembly in _pluginAssemblies)
+ foreach (var plugin in _preloadedPlugins)
{
+ var assembly = plugin.Assembly;
var assemblyName = assembly.GetName().Name;
if (assemblyName is null)
continue;
@@ -44,26 +49,26 @@ namespace BTCPayServer.Plugins
if (exception.Source is not null &&
assemblyName.Equals(exception.Source, StringComparison.Ordinal))
{
- pluginName = assemblyName;
+ preloadedPlugin = plugin;
return true;
}
if (exception.Message.Contains(assemblyName, StringComparison.Ordinal))
{
- pluginName = assemblyName;
+ preloadedPlugin = plugin;
return true;
}
// For TypeLoadException, check if it might come from areferenced assembly
if (!string.IsNullOrEmpty(fromAssembly) && assembly.GetReferencedAssemblies().Select(a => a.Name).Contains(fromAssembly))
{
- pluginName = assemblyName;
+ preloadedPlugin = plugin;
return true;
}
}
- pluginName = null;
+ preloadedPlugin = null;
return false;
}
- record PreloadedPlugin(IBTCPayServerPlugin Instance, PluginLoader? Loader, Assembly Assembly);
+ public record PreloadedPlugin(IBTCPayServerPlugin Instance, PluginLoader? Loader, Assembly Assembly);
class PreloadedPlugins : IEnumerable<PreloadedPlugin>
{
@@ -113,10 +118,9 @@ namespace BTCPayServer.Plugins
public static IMvcBuilder AddPlugins(this IMvcBuilder mvcBuilder, IServiceCollection serviceCollection,
IConfiguration config, ILoggerFactory loggerFactory, ServiceProvider bootstrapServiceProvider)
{
-
+ var preloadedPlugins = new PreloadedPlugins();
var logger = loggerFactory.CreateLogger(typeof(PluginManager));
var pluginsFolder = new DataDirectories().Configure(config).PluginDir;
- var preloadedPlugins = new PreloadedPlugins();
serviceCollection.Configure<KestrelServerOptions>(options =>
{
@@ -177,6 +181,7 @@ namespace BTCPayServer.Plugins
{
if (preloadedPlugins.Contains(toLoad.PluginIdentifier))
continue;
+
try
{
var loader = PluginLoader.CreateFromAssemblyFile(
@@ -229,7 +234,6 @@ namespace BTCPayServer.Plugins
GetPluginInstanceFromAssembly(plugin.Identifier, preloadedPlugin.Assembly, silentlyFails: false);
if (preloadedPlugin.Loader is not null)
mvcBuilder.AddPluginLoader(preloadedPlugin.Loader);
- _pluginAssemblies.Add(preloadedPlugin.Assembly);
logger.Log(plugin.SystemPlugin ? LogLevel.Debug : LogLevel.Information,
$"Adding and executing plugin {plugin.Identifier} - {plugin.Version}");
@@ -250,6 +254,7 @@ namespace BTCPayServer.Plugins
toDisable.Add(plugin.Identifier);
}
}
+ _preloadedPlugins = preloadedPlugins;
if (toDisable.Count > 0)
{
foreach (var plugin in toDisable)
@@ -486,6 +491,20 @@ namespace BTCPayServer.Plugins
QueueCommands(pluginDir, cmds);
}
+ public static void DisablePlugins(string pluginDir)
+ {
+ foreach (var plugin in _preloadedPlugins)
+ DisablePlugin(pluginDir, plugin);
+ }
+
+ public static void DisablePlugin(string pluginDir, PreloadedPlugin plugin)
+ {
+ if (plugin.Instance.SystemPlugin) return;
+ var name = plugin.Assembly.GetName()?.Name;
+ if (name is null) return;
+ DisablePlugin(pluginDir, name);
+ }
+
public static void DisablePlugin(string pluginDir, string plugin)
{
QueueCommands(pluginDir, ("disable", plugin));
diff --git a/BTCPayServer/Program.cs b/BTCPayServer/Program.cs
index 1cddce6..533a83a 100644
--- a/BTCPayServer/Program.cs
+++ b/BTCPayServer/Program.cs
@@ -97,11 +97,20 @@ namespace BTCPayServer
if (!string.IsNullOrEmpty(ex.Message))
logs.Configuration.LogError(ex.Message);
}
- catch (Exception e) when (PluginManager.IsExceptionByPlugin(e, out var pluginName))
+ catch (Exception e) when (PluginManager.IsExceptionByPlugin(e, out var plugin))
{
- logs.Configuration.LogError(e, $"Plugin crash during startup detected, disabling {pluginName}...");
var pluginDir = new DataDirectories().Configure(conf).PluginDir;
- PluginManager.DisablePlugin(pluginDir, pluginName);
+ // This happen when a plugin fails to resolve some dependencies in startup services
+ if (plugin.Instance.SystemPlugin && e.Source == "Microsoft.Extensions.DependencyInjection")
+ {
+ logs.Configuration.LogError(e, "Plugin crash during startup detected. We couldn't figure out which plugin caused it, disabling all plugins.");
+ PluginManager.DisablePlugins(pluginDir);
+ }
+ else
+ {
+ logs.Configuration.LogError(e, $"Plugin crash during startup detected, disabling {plugin.Assembly.GetName().Name}...");
+ PluginManager.DisablePlugin(pluginDir, plugin);
+ }
}
finally
{
Why this scored 31/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.