PaperMC / PaperMC/Paper

Classloader of a plugin being disabled has a tiny window where it is accessible in the wrong state

Open
#13,020 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: needs triage version: 1.21.8
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.