NationalSecurityAgency/ghidra · error · RuntimeError

Transaction already started

Error message

Transaction already started

What it means

Raised by State.require_no_tx() (commands.py:152-154) when STATE.tx is already a live Transaction. The agent forbids nesting transactions on a single trace: only one tx may be open at a time. Attempting to open a second tx (e.g. via ghidra_trace_tx_start) while the previous one is uncommitted triggers this.

Source

Thrown at Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/commands.py:154

    def require_no_trace(self) -> None:
        if self.trace != None:
            raise RuntimeError("Trace already started")

    def reset_trace(self) -> None:
        self.trace: Optional[Trace[Extra]] = None
        util.set_convenience_variable('_ghidra_tracing', "false")
        self.reset_tx()

    def require_tx(self) -> Tuple[Trace, Transaction]:
        trace = self.require_trace()
        if self.tx is None:
            raise RuntimeError("No transaction")
        return trace, self.tx

    def require_no_tx(self) -> None:
        if self.tx != None:
            raise RuntimeError("Transaction already started")

    def reset_tx(self) -> None:
        self.tx: Optional[Transaction] = None


STATE = State()


def ghidra_trace_connect(address: Optional[str] = None) -> None:
    """Connect Python to Ghidra for tracing.

    Address must be of the form 'host:port'
    """

    STATE.require_no_client()
    if address is None:
        raise RuntimeError(
            "'ghidra_trace_connect': missing required argument 'address'")

View on GitHub (pinned to d5f144c24d)

Solutions

  1. Ensure each ghidra_trace_tx_start() has exactly one matching ghidra_trace_tx_commit() (or ghidra_trace_tx_abort()) before opening another.
  2. If STATE.tx may be stale, call ghidra_trace_tx_abort() defensively once, then retry tx_start.
  3. Factor helpers so they never open a transaction themselves — accept the tx from the caller.

Example fix

// before
def write_block():
    ghidra_trace_tx_start('block')   # collides if caller already opened one
    ghidra_trace_putmem(a, l)
    ghidra_trace_tx_commit()

// after
def write_block():
    ghidra_trace_putmem(a, l)   # caller owns the tx

# caller:
ghidra_trace_tx_start('batch')
write_block(); write_block()
ghidra_trace_tx_commit()
Defensive patterns

Strategy: validation

Validate before calling

from ghidradbg.commands import STATE
if getattr(STATE, 'tx', None) is not None:
    raise RuntimeError('a transaction is already open; commit or abort first')

Type guard

def tx_is_closed() -> bool:
    from ghidradbg.commands import STATE
    return getattr(STATE, 'tx', None) is None

Try / catch

try:
    ghidra_trace_tx_start('op')
except RuntimeError as e:
    if 'Transaction already started' in str(e):
        ghidra_trace_tx_commit()   # or abort
        ghidra_trace_tx_start('op')
    else:
        raise

Prevention

When it happens

Trigger: Calling ghidra_trace_tx_start() twice in a row without an intervening ghidra_trace_tx_commit() or ghidra_trace_tx_abort(); start_trace() being re-entered while a schema tx is still open; recursive helper that opens its own tx while the caller's tx is still open.

Common situations: Refactor that wrapped putmem in a helper which itself opens a tx, colliding with the caller's tx; exception in the middle of a tx block that left STATE.tx set and a retry tried to reopen; mismatched tx_start/tx_commit counts in a loop.

Related errors


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