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 informationView on GitHub (pinned to d5f144c24d)
Solutions
- Keep the entire suppress-within try block on a single thread (open and close together).
- Never store or pass the Suppression handle across threads.
- 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
- Never return or pass a Suppression across threads.
- Keep the try-with-resources block entirely on one thread.
- Re-open a suppression per thread when work is parallelized.
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
- Private queue no longer exists
- Failed to read memory
- Timed out reading or writing target
- Error reading or writing target
- Cannot emulate a trace unless it's opened in the tool.
AI-assisted analysis of NationalSecurityAgency/ghidra@d5f144c24d (2026-08-14).
Data as JSON: /api/errors/da6bc5b8d207489b.
Report an issue: GitHub.