Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 29 additions & 16 deletions libbs/decompilers/ida/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,15 @@ def get_types(structs=True, enums=True, typedefs=True) -> typing.Dict[str, Artif

if structs and tif.is_struct():
bs_struct = bs_struct_from_tif(tif)
types[bs_struct.name] = bs_struct
# IDA exposes nested types inside anonymous unions/structs as separate
# numbered types whose name is "$PARENT_HASH::member" — that qualified
# form can't be looked up via get_named_type_tid, so skip it.
if bs_struct.name and "::" not in bs_struct.name:
types[bs_struct.name] = bs_struct
elif enums and tif.is_enum():
bs_enum = enum_from_tif(tif)
types[bs_enum.name] = bs_enum
if bs_enum is not None:
types[bs_enum.name] = bs_enum

return types

Expand Down Expand Up @@ -1403,10 +1408,11 @@ def _deprecated_get_enum_mmebers(_enum_id, max_size=100) -> typing.Dict[str, int
return enum_members


def get_enum_members(_enum: typing.Union["ida_typeinf.tinfo_t", int], max_size=100) -> typing.Dict[str, int]:
def get_enum_members(_enum: typing.Union["ida_typeinf.tinfo_t", int], max_size=100) -> typing.Optional[typing.Dict[str, int]]:
"""
_enum can either be an ida_typeinf.tinfo_t or an int (the old enum id system)

_enum can either be an ida_typeinf.tinfo_t or an int (the old enum id system).
Returns None if the tif reports as an enum but IDA can't fetch its details
(e.g. typedef wrappers that pass tif.is_enum() but aren't real enums).
"""
if not new_ida_typing_system():
_enum_id: int = _enum
Expand All @@ -1416,8 +1422,8 @@ def get_enum_members(_enum: typing.Union["ida_typeinf.tinfo_t", int], max_size=1
enum_tif: "ida_typeinf.tinfo_t" = _enum
ei = ida_typeinf.enum_type_data_t()
if not enum_tif.get_enum_details(ei):
_l.error("IDA failed to get enum details for %s", enum_tif)
return {}
_l.debug("IDA could not get enum details for %s; treating as non-enum", enum_tif)
return None

enum_members = {}
for e_memb in ei:
Expand All @@ -1442,6 +1448,8 @@ def enum_from_tif(tif):
return None

enum_members = get_enum_members(tif)
if enum_members is None:
return None
return Enum(enum_name, enum_members)


Expand All @@ -1459,6 +1467,8 @@ def enum(name) -> typing.Optional[Enum]:

enum_name = str(_enum.get_type_name()) if new_enums else idc.get_enum_name(_enum)
enum_members = get_enum_members(_enum)
if enum_members is None:
return None
return Enum(enum_name, enum_members)


Expand Down Expand Up @@ -1736,18 +1746,17 @@ def has_older_hexrays_version():
@execute_write
def get_decompiler_version() -> typing.Optional[Version]:
wait_for_idc_initialization()
try:
_vers = ida_hexrays.get_hexrays_version()
except Exception as e:
_l.critical("Failed to get decompiler version: %s", e)
return None

try:
vers = Version(_vers)
except TypeError:
# init_hexrays_plugin() must succeed before any other ida_hexrays.* call —
# otherwise IDA emits "Hex-Rays Decompiler got called from Python without
# being loaded" warnings (e.g. during early plugin load before Hex-Rays
# finishes wiring up). Returns False if the decompiler is genuinely
# unavailable (headless without license, etc.); the caller should treat
# None as "decompiler unavailable, skip version-gated behavior".
if not ida_hexrays.init_hexrays_plugin():
return None

return vers
return Version(ida_hexrays.get_hexrays_version())


#
Expand Down Expand Up @@ -1902,6 +1911,10 @@ def run(self, arg):
pass

def term(self):
try:
self.interface._term_gui_hooks()
except Exception:
_l.exception("Error tearing down GUI hooks")
self.interface.decompiler_closed_event()
del self.interface

Expand Down
25 changes: 25 additions & 0 deletions libbs/decompilers/ida/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,25 @@ def refresh_pseudocode(self, vu):
self._send_decompilation_event(vu.cfunc)
return 0

def curpos(self, vu):
# Hex-Rays cursor moved within pseudocode. View_Hooks.view_curpos doesn't
# fire reliably for pseudocode caret motion (esp. arrow-key navigation),
# so mirror it through to the same context-update path the disassembly
# view uses, so "users on current function" updates promptly.
if not (self.interface.force_click_recording or self.interface.artifact_watchers_started):
return 0
widget = vu.ct if hasattr(vu, "ct") else None
if widget is None:
return 0
ctx = compat.view_to_bs_context(widget, action=Context.ACT_VIEW_OPEN)
if ctx is None:
return 0
ctx = self.interface.art_lifter.lift(ctx)
ctx.last_change = datetime.datetime.now(tz=datetime.timezone.utc)
self.interface._gui_active_context = ctx
self.interface.gui_context_changed(ctx)
return 0

#
# helpers
#
Expand Down Expand Up @@ -636,6 +655,12 @@ def view_click(self, view, event):
def view_activated(self, view: "TWidget *"):
self._handle_view_event(view, action_type=Context.ACT_VIEW_OPEN)

def view_curpos(self, view: "TWidget *"):
# fires when the cursor (current position) moves within the view —
# includes keyboard navigation (G/jump, arrow keys, double-clicking
# in the Functions list, etc.), which view_click/view_activated miss.
self._handle_view_event(view, action_type=Context.ACT_VIEW_OPEN)

def view_mouse_moved(self, view: "TWidget *", event: "view_mouse_event_t"):
if self.interface.track_mouse_moves:
self._handle_view_event(view, ida_event=event, action_type=Context.ACT_MOUSE_MOVE)
Expand Down
17 changes: 15 additions & 2 deletions libbs/decompilers/ida/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ def _init_gui_hooks(self):
for hook in self._ui_hooks:
hook.hook()

def _term_gui_hooks(self):
"""
Symmetric teardown for _init_gui_hooks. Must run before IDAPython tears
down — otherwise a still-registered hook can fire during shutdown
events (e.g. term_database) and try to re-enter a finalized Python.
"""
for hook in self._ui_hooks:
try:
hook.unhook()
except Exception:
_l.exception("Failed to unhook %r", hook)
self._ui_hooks = []

def _init_gui_plugin(self, *args, **kwargs):
self.decompiler_opened_event()
plugin_cls_name = self._plugin_name + "_cls"
Expand Down Expand Up @@ -246,7 +259,7 @@ def start_artifact_watchers(self):
super().start_artifact_watchers()
# TODO: this is a hack for backwards compatibility and should be removed in IDA 9
idb_hook = IDBHooks(self)
if self.dec_version < Version("8.4"):
if self.decompiler_available and self.dec_version < Version("8.4"):
idb_hook.local_types_changed = lambda: 0
else:
# this code in this block must exist in 9.0, so don't delete it!
Expand Down Expand Up @@ -355,7 +368,7 @@ def _global_vars(self, **kwargs) -> Dict[int, GlobalVariable]:
# structs
def _set_struct(self, struct: Struct, header=True, members=True, **kwargs) -> bool:
data_changed = False
if (self.dec_version < Version("8.3")) and "gcc_va_list" in struct.name:
if self.decompiler_available and self.dec_version < Version("8.3") and "gcc_va_list" in struct.name:
_l.critical("Syncing the struct %s in IDA Pro 8.2 <= will cause a crash. Skipping...", struct.name)
return False

Expand Down
4 changes: 4 additions & 0 deletions libbs/ui/qt_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
QComboBox,
QDialog,
QFileDialog,
QFormLayout,
QGridLayout,
QGroupBox,
QHBoxLayout,
Expand Down Expand Up @@ -49,6 +50,7 @@
QToolTip,
QStackedLayout,
QDateTimeEdit,
QSplitter,
)
from PySide6.QtGui import (
QFontDatabase,
Expand Down Expand Up @@ -80,6 +82,7 @@
QComboBox,
QDialog,
QFileDialog,
QFormLayout,
QGridLayout,
QGroupBox,
QHBoxLayout,
Expand Down Expand Up @@ -118,6 +121,7 @@
QToolTip,
QStackedLayout,
QDateTimeEdit,
QSplitter,
)
from PyQt5.QtGui import (
QFontDatabase,
Expand Down
Loading