From a285fe839998a4f5b1ccf08236369b37b8ba3e4f Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Mon, 18 May 2026 23:30:20 -0400 Subject: [PATCH] try to fix BQL bug --- pyplugins/compat/qemu_compat.py | 31 ++++++++++++++++++++++++--- pyplugins/interventions/remotectrl.py | 13 +++++++---- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/pyplugins/compat/qemu_compat.py b/pyplugins/compat/qemu_compat.py index abf3b323b..9d89c31fe 100644 --- a/pyplugins/compat/qemu_compat.py +++ b/pyplugins/compat/qemu_compat.py @@ -1,5 +1,6 @@ import os import shlex +from contextlib import contextmanager from pathlib import Path from typing import Callable, List, Optional @@ -481,6 +482,26 @@ def _lib_symbol(self, name: str): except AttributeError: return None + @contextmanager + def bql_held(self, callsite: bytes = b"pyplugins/qemu_compat.py"): + """Acquire the BQL on entry if the current thread doesn't already hold it. + + Safe to call from any thread. KVM vCPU threads, the remotectrl asyncio + thread, and other non-main-loop threads can use this to invoke QEMU + APIs that assert(bql_locked()) — e.g. memory_region_transaction_commit, + cpu_memory_rw_debug, plugin/listener registration. + """ + bql_locked = self._lib_symbol("bql_locked") + if bql_locked is None or bql_locked(): + yield + return + self.lib.bql_lock_impl(callsite, 0) + try: + yield + finally: + if bql_locked(): + self.lib.bql_unlock() + def set_hypercall_callback(self, cb: Callable): if self.mode == "kvm": ctype = ( @@ -511,10 +532,14 @@ def _dispatch_hypercall(self, cs, nr, a0, a1, a2, a3, a4, a5, ret_ptr, opaque=No self._current_ret_ptr = ret_ptr self._current_retval = 0 + # KVM vCPU threads drop BQL across KVM_RUN and most exit handlers don't + # reacquire it, but our pyplugin handlers call BQL-protected APIs + # (cpu_memory_rw_debug, memory_region_*, etc). Re-acquire here. try: - plugin = self.hypercall_plugin - if plugin is not None: - return plugin.dispatch(cs, nr, ret_ptr) + with self.bql_held(b"pyplugins/qemu_compat.py:_dispatch_hypercall"): + plugin = self.hypercall_plugin + if plugin is not None: + return plugin.dispatch(cs, nr, ret_ptr) finally: self._current_ret_ptr = self.ffi.NULL diff --git a/pyplugins/interventions/remotectrl.py b/pyplugins/interventions/remotectrl.py index e8488d654..a27d983ce 100644 --- a/pyplugins/interventions/remotectrl.py +++ b/pyplugins/interventions/remotectrl.py @@ -199,11 +199,16 @@ def _process_message(self, data): cmd_type = cmd.get('type') handler = self.handlers.get(cmd_type) - if handler: - result = handler(cmd) - return {"status": "success", **(result if isinstance(result, dict) else {})} - else: + if not handler: return {"status": "error", "message": f"Unknown command: {cmd_type}"} + + # Handlers run on the asyncio thread, but loading pyplugins, + # registering callbacks, or reading guest memory require the BQL. + # Hold it across the whole dispatch so any cascading PANDA/QEMU + # API hits assert(bql_locked()) cleanly. + with self.panda.bql_held(b"pyplugins/remotectrl.py:_process_message"): + result = handler(cmd) + return {"status": "success", **(result if isinstance(result, dict) else {})} except Exception as e: self.logger.error(traceback.format_exc()) return {"status": "error", "message": str(e)}