NationalSecurityAgency/ghidra · error · RuntimeError

Transaction already started

Error message

Transaction already started

What it means

Raised by State.require_no_tx() (commands.py:149-151) when STATE.tx is already non-None. The x64dbg trace agent keeps a single global STATE singleton holding at most one open Transaction; require_no_tx() is a precondition guard invoked by commands that must begin a fresh transaction (e.g. trace-start paths). A second start while the first is uncommitted trips the guard.

Source

Thrown at Ghidra/Debug/Debugger-agent-x64dbg/src/main/py/src/ghidraxdbg/commands.py:151

    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. Close/commit the existing transaction first (call ghidra_trace_tx_end, or whichever end/commit your command provides) before starting a new one.
  2. Call STATE.reset_tx() in a finally block so an aborted transaction never leaks into the next command.
  3. Guard before starting: check 'if STATE.tx is not None' and end it before opening another.
  4. Wrap each transactional command body in try/finally that ends the tx on every exit path.

Example fix

// before
ghidra_trace_tx_start()
# ... error here, tx never closed ...
ghidra_trace_tx_start()  # -> Transaction already started

// after
if STATE.tx is not None:
    ghidra_trace_tx_end()
ghidra_trace_tx_start()
Defensive patterns

Strategy: validation

Validate before calling

def safe_tx_start():
    if STATE.tx is not None:
        raise RuntimeError(f"Refusing to start tx: one is open already ({STATE.tx})")
    # ... open the transaction ...

Type guard

def tx_is_open() -> bool:
    return STATE.tx is not None

Try / catch

try:
    ghidra_trace_tx_start()
except RuntimeError as e:
    if 'already started' in str(e):
        ghidra_trace_tx_end()
        ghidra_trace_tx_start()
    else:
        raise

Prevention

When it happens

Trigger: Calling a command that calls STATE.require_no_tx() while a previous transaction is still open: e.g. ghidra_trace_tx_start invoked twice without a matching ghidra_trace_tx_end, or ghidra_trace_start after a tx_start with no tx_end/commit. Any nested transaction attempt on the singleton STATE.

Common situations: A script that opens a tx, hits an error before reaching the end/commit, then retries the same command; forgetting to pair tx_start with tx_end; running two trace commands in sequence where the first left the tx open due to an exception that was swallowed.

Related errors


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