NationalSecurityAgency/ghidra · error · WrongThreadException

Was {}. Want {}

Error message

Was {}. Want {}

What it means

Raised as WrongThreadException by the GhidraDbg._base property when the calling thread is not the dedicated dbgeng engine thread. DbgEng COM objects are single-threaded (STA); accessing them from any other thread corrupts state, so the agent enforces thread affinity.

Source

Thrown at Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/util.py:289

        remote = os.getenv('OPT_CONNECT_STRING')
        if remote is not None:
            remote_client = DbgEng.DebugConnect(remote)
            debug_client = self._generate_client(remote_client)
            self._protected_base = AllDbg(client=debug_client)
        else:
            self._protected_base = AllDbg()

    def _generate_client(self, original: DebugClient) -> DebugClient:
        cli = POINTER(DbgEng.IDebugClient)()
        cliptr = POINTER(POINTER(DbgEng.IDebugClient))(cli)
        hr = original.CreateClient(cliptr)
        exception.check_err(hr)
        return DebugClient(client=cli)

    @property
    def _base(self) -> AllDbg:
        if threading.current_thread() is not self._thread:
            raise WrongThreadException("Was {}. Want {}".format(
                threading.current_thread(), self._thread))
        return self._protected_base

    def run(self, fn: Callable[..., T], *args, **kwargs) -> T:
        # TODO: Remove this check?
        if hasattr(self, '_thread') and threading.current_thread() is self._thread:
            raise WrongThreadException()
        future = self._queue.submit(fn, *args, **kwargs)
        # https://stackoverflow.com/questions/72621731/is-there-any-graceful-way-to-interrupt-a-python-concurrent-future-result-call gives an alternative
        while True:
            try:
                return future.result(0.5)
            except concurrent.futures.TimeoutError:
                pass

    def run_async(self, fn: Callable[..., T], *args, **kwargs) -> Future[T]:
        return self._queue.submit(fn, *args, **kwargs)

View on GitHub (pinned to d5f144c24d)

Solutions

  1. Annotate any function touching dbg._base with @util.dbg.eng_thread (or call it through dbg.run(...)).
  2. Ensure background work submits onto the engine thread instead of calling COM inline.
  3. If you must call from another thread, route through dbg.run(fn, *args) which marshals via the engine queue.

Example fix

// before
def my_op():
    return dbg._base.reg.get_pc()  # may run off-thread -> WrongThreadException
// after
@util.dbg.eng_thread
def my_op():
    return dbg._base.reg.get_pc()
Defensive patterns

Strategy: validation

Validate before calling

import threading
if threading.current_thread() is not dbg._thread:
    raise RuntimeError('must run on the dbgeng engine thread; decorate with @util.dbg.eng_thread')

Type guard

import threading

def on_engine_thread() -> bool:
    return threading.current_thread() is getattr(dbg, '_thread', None)

Try / catch

from ghidradbg.util import WrongThreadException
try:
    val = dbg._base.reg.get_pc()
except WrongThreadException:
    val = dbg.run(lambda: dbg._base.reg.get_pc())  # marshal onto engine thread

Prevention

When it happens

Trigger: Touching dbg()._base / dbg._base from a worker thread, a GUI callback, or any code path not decorated with @dbg.eng_thread (or @dbg.check_thread). The decorator marshals the call onto the engine thread; bypassing it triggers this guard.

Common situations: Adding new agent code that calls dbg()._base directly without the eng_thread decorator; invoking DbgEng operations from a Python background thread or timer; refactoring that drops the decorator.

Related errors


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