Classloader of a plugin being disabled has a tiny window where it is accessible in the wrong state
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 12.7k
- Forks
- 3.5k
- Avg merge
- 3d 13h
- Merged PRs (30d)
- 11
Description
Expected behavior
When a plugin innocently calls Class.ofName() or a similar method that iterates through a list of enabled plugin classloaders, it should not unexpectedly get an IllegalStateException exception due to another plugin being disabled.
Observed/Actual behavior
Calling Class.ofName() or a similar method from a separate thread has a tiny chance of triggering an IllegalStateException exception if a plugin happens to be getting disabled at the time of the call.
An example of a situation where this may occur is when a plugin needs to do its time-consuming initialization in a separate thread to avoid holding up the server's main thread during startup, as is the case for a popular plugin named DiscordSRV.
Steps/models to reproduce
Create a new plugin and paste the following code.
private ItemStack uninitializedStack;
@Override
public void onEnable() {
/*
If a plugin has more setup to do that may hold up the server's main thread for too long,
it would be done in a separate thread instead.
If it calls a certain method on the new thread, it has a tiny chance of unexpectedly accessing
a disabling plugin's classloader before the classloader is unregistered but after it has
closed its jar file, triggering an IllegalStateException exception.
*/
new Thread(this::loop, "Extended Startup Plugin").start();
// Pretend this is another plugin that experiences an unpredicted error during startup and gets force disabled.
// The bug couldn't be reproduced if the plugin just disables itself directly.
uninitializedStack.getType();
}
private void loop() {
try {
/*
This repeatedly attempts to load a class that doesn't exist to force Paper to iterate through all plugin
classloaders in hope that it would access the classloader of a disabled plugin before it is unregistered.
*/
while (true) {
try {
Class.forName("io.github.NonexistentClass");
} catch (ClassNotFoundException ignored) {}
}
} catch (IllegalStateException e) {
this.getLogger().log(Level.SEVERE, "Caught a disabled plugin classloader before it gets unregistered!", e);
}
}
Install this plugin on a clean server instance. Then repeatedly start up and shut down the server until the plugin logs a caught IllegalStateException. This can take upward of 20 tries.
Plugin and Datapack List
No datapacks and just the plugin I provided in the "Steps to reproduce" section.
Paper version
[20:07:03 INFO]: This server is running Paper version 1.21.8-49-main@614e9ac (2025-08-23T21:22:35Z) (Implementing API version 1.21.8-R0.1-SNAPSHOT)
You are running the latest version
Previous version: 1.21.7-15-0cadaef (MC: 1.21.7)
Other
I don't know exactly when this bug arose, but I first discovered it randomly on Paper 1.21.6 while testing a plugin I made which happened to have a flawed startup logic that causes it to be disabled immediately on startup. To my puzzlement, another plugin shortly after that also threw an exception due to the first plugin being disabled, though both plugins had nothing to do with each other. That led me down a rabbit hole seeking an explanation for the odd behavior.
The issues lies in how Paper disables a plugin. It first calls ClassLoader.close() which closes the plugin jar, and then unregisters the plugin classloader, creating a small opening where the classloader is accessible in the wrong state.
This is Paper-only issue as Spigot disables the plugin in a correct order so that the plugin will never be accessed in the wrong state. It should be easily resolved by unregistering the plugin classloader before closing it, unless there is a reason the disable logic is the way it is.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in paper-server/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java at the disable logic linked in the issue, and trace how plugin classloaders are closed and unregistered. Reproduce the race with the provided plugin and repeated server startup/shutdown. Done means the classloader cannot be accessed after closing and before unregistration, without breaking plugin disable behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100