{"record":{"id":"da6bc5b8d207489b","repo":"NationalSecurityAgency/ghidra","slug":"must-close-on-the-same-thread-as-suppressed-the-ca","errorCode":null,"errorMessage":"Must close on the same thread as suppressed the callback","messagePattern":"Must close on the same thread as suppressed the callback","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/SuppressableCallback.java","lineNumber":85,"sourceCode":" */\npublic class SuppressableCallback<T> {\n\t/**\n\t * A suppression handle on the callback, for a specific thread\n\t */\n\tpublic static class Suppression implements AutoCloseable {\n\t\tprivate final SuppressableCallback<?> cb;\n\t\tprivate final Thread thread;\n\n\t\tprivate <T> Suppression(SuppressableCallback<T> cb, Thread thread, T value) {\n\t\t\tthis.cb = cb;\n\t\t\tthis.thread = thread;\n\t\t\tcb.stack.get().push(value);\n\t\t}\n\n\t\t@Override\n\t\tpublic void close() {\n\t\t\tif (thread != Thread.currentThread()) {\n\t\t\t\tthrow new IllegalStateException(\n\t\t\t\t\t\"Must close on the same thread as suppressed the callback\");\n\t\t\t}\n\t\t\tcb.stack.get().pop();\n\t\t}\n\t}\n\n\t// Carry a cached read-only view with the list\n\tprivate static class ListWithView<T> extends LinkedList<T> {\n\t\tprivate final List<T> view = Collections.unmodifiableList(this);\n\t}\n\n\t/**\n\t * The stack of values from each suppression, probably just one\n\t */\n\tprivate final ThreadLocal<ListWithView<T>> stack = ThreadLocal.withInitial(ListWithView::new);\n\n\t/**\n\t * Suppress this callback, providing the given value as information","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/NationalSecurityAgency/ghidra/blob/d5f144c24d6bc53c9cbf4448c6d11143e7696206/Ghidra/Debug/ProposedUtils/src/main/java/utilities/util/SuppressableCallback.java#L67-L103","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (opens on thread A, closes on thread B)\nSuppressableCallback<?>.Suppression s = cb.suppress(value);\nexecutor.submit(() -> { try { ... } finally { s.close(); } });\n// after (open and close on the same thread)\nexecutor.submit(() -> {\n    try (var s = cb.suppress(value)) {\n        ...\n    }\n});","handlingStrategy":"validation","validationCode":"// Open and close suppression within the same thread:\nThread t = Thread.currentThread();\ntry (var s = cb.suppress(value)) {\n    assert Thread.currentThread() == t; // do not hop threads inside this block\n    work();\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["concurrency","threading","lifecycle","callback"],"backgroundTag":null,"analyzedSha":"d5f144c24d6bc53c9cbf4448c6d11143e7696206","analyzedAt":"2026-08-14T01:00:57.564Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}