NationalSecurityAgency/ghidra · error · IllegalStateException

Must close on the same thread as suppressed the callback

Error message

Must close on the same thread as suppressed the callback

What it means

SuppressableCallback.Suppression is opened on a thread (recorded in `this.thread`) and pushes a value onto a thread-local stack. close() pops that same stack; popping from a different thread would corrupt that thread's stack, so IllegalStateException is thrown when Thread.currentThread() differs from the opening thread. The suppression is therefore thread-affine.

Source

Thrown at Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/SuppressableCallback.java:85

 */
public class SuppressableCallback<T> {
	/**
	 * A suppression handle on the callback, for a specific thread
	 */
	public static class Suppression implements AutoCloseable {
		private final SuppressableCallback<?> cb;
		private final Thread thread;

		private <T> Suppression(SuppressableCallback<T> cb, Thread thread, T value) {
			this.cb = cb;
			this.thread = thread;
			cb.stack.get().push(value);
		}

		@Override
		public void close() {
			if (thread != Thread.currentThread()) {
				throw new IllegalStateException(
					"Must close on the same thread as suppressed the callback");
			}
			cb.stack.get().pop();
		}
	}

	// Carry a cached read-only view with the list
	private static class ListWithView<T> extends LinkedList<T> {
		private final List<T> view = Collections.unmodifiableList(this);
	}

	/**
	 * The stack of values from each suppression, probably just one
	 */
	private final ThreadLocal<ListWithView<T>> stack = ThreadLocal.withInitial(ListWithView::new);

	/**
	 * Suppress this callback, providing the given value as information

View on GitHub (pinned to d5f144c24d)

Solutions

  1. Keep the entire suppress-within try block on a single thread (open and close together).
  2. Never store or pass the Suppression handle across threads.
  3. If work crosses threads, re-open a new suppression on each thread instead.

Example fix

// before (opens on thread A, closes on thread B)
SuppressableCallback<?>.Suppression s = cb.suppress(value);
executor.submit(() -> { try { ... } finally { s.close(); } });
// after (open and close on the same thread)
executor.submit(() -> {
    try (var s = cb.suppress(value)) {
        ...
    }
});
Defensive patterns

Strategy: validation

Validate before calling

// Open and close suppression within the same thread:
Thread t = Thread.currentThread();
try (var s = cb.suppress(value)) {
    assert Thread.currentThread() == t; // do not hop threads inside this block
    work();
}

Prevention

When it happens

Trigger: Opening a suppression via try-with-resources on thread A and the AutoCloseable closing on thread B - e.g. returning the Suppression from an executor task, or a CompletableFuture/async completion closing it.

Common situations: Using SuppressableCallback inside ExecutorService/CompletableFuture tasks where open and close land on different pool threads; passing the suppression handle across thread boundaries; unit tests that open on the test thread but close on a worker.

Related errors


AI-assisted analysis of NationalSecurityAgency/ghidra@d5f144c24d (2026-08-14). Data as JSON: /api/errors/da6bc5b8d207489b. Report an issue: GitHub.