Skip to content

Commit 112034c

Browse files
committed
bugfix: fixed a race condition that would crash lighthtouse when saving compositions
1 parent 58f1260 commit 112034c

File tree

2 files changed

+95
-23
lines changed

2 files changed

+95
-23
lines changed

plugin/lighthouse/director.py

+34-7
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def __init__(self, palette):
154154
#----------------------------------------------------------------------
155155

156156
self._ast_queue = Queue.Queue()
157+
self._composition_lock = threading.Lock()
157158
self._composition_cache = CompositionCache()
158159

159160
self._composition_worker = threading.Thread(
@@ -693,8 +694,6 @@ def add_composition(self, composite_name, ast):
693694

694695
# evaluate the last AST into a coverage set
695696
composite_coverage = self._evaluate_composition(ast)
696-
composite_coverage.update_metadata(self.metadata)
697-
composite_coverage.refresh() # TODO: hash refresh?
698697

699698
# save the evaluated coverage under the given name
700699
self._update_coverage(composite_name, composite_coverage)
@@ -741,15 +740,14 @@ def _async_evaluate_ast(self):
741740
# produce a single composite coverage object as described by the AST
742741
composite_coverage = self._evaluate_composition(ast)
743742

744-
# map the composited coverage data to the database metadata
745-
composite_coverage.update_metadata(self.metadata)
746-
composite_coverage.refresh()
747-
748743
# we always save the most recent composite to the hotshell entry
749744
self._special_coverage[HOT_SHELL] = composite_coverage
750745

746+
#
751747
# if the hotshell entry is the active coverage selection, notify
752748
# listeners of its update
749+
#
750+
753751
if self.coverage_name == HOT_SHELL:
754752
self._notify_coverage_modified()
755753

@@ -767,8 +765,37 @@ def _evaluate_composition(self, ast):
767765
if isinstance(ast, TokenNull):
768766
return self._NULL_COVERAGE
769767

768+
#
769+
# the director's composition evaluation code (this function) is most
770+
# generally called via the background caching evaluation thread known
771+
# as self._composition_worker. But this function can also be called
772+
# inline via the 'add_composition' function from a different thread
773+
# (namely, the main thread)
774+
#
775+
# because of this, we must control access to the resources the AST
776+
# evaluation code operates by restricting the code to one thread
777+
# at a time.
778+
#
779+
# should we call _evaluate_composition from the context of the main
780+
# IDA thread, it is important that we do so in a pseudo non-blocking
781+
# such that we don't hang IDA. await_lock(...) will allow the Qt/IDA
782+
# main thread to yield to other threads while waiting for the lock
783+
#
784+
785+
await_lock(self._composition_lock)
786+
770787
# recursively evaluate the AST
771-
return self._evaluate_composition_recursive(ast)
788+
composite_coverage = self._evaluate_composition_recursive(ast)
789+
790+
# map the composited coverage data to the database metadata
791+
composite_coverage.update_metadata(self.metadata)
792+
composite_coverage.refresh() # TODO: hash refresh?
793+
794+
# done operating on shared data (coverage), release the lock
795+
self._composition_lock.release()
796+
797+
# return the evaluated composition
798+
return composite_coverage
772799

773800
def _evaluate_composition_recursive(self, node):
774801
"""

plugin/lighthouse/util/ida.py

+61-16
Original file line numberDiff line numberDiff line change
@@ -475,23 +475,19 @@ def thunk():
475475
# IDA Async Magic
476476
#------------------------------------------------------------------------------
477477

478-
@mainthread
479-
def await_future(future, block=True, timeout=1.0):
478+
def await_future(future):
480479
"""
481480
This is effectively a technique I use to get around completely blocking
482481
IDA's mainthread while waiting for a threaded result that may need to make
483-
use of the sync operators.
482+
use of the execute_sync operators.
484483
485484
Waiting for a 'future' thread result to come through via this function
486485
lets other execute_sync actions to slip through (at least Read, Fast).
487486
"""
488-
489-
elapsed = 0 # total time elapsed processing this future object
490487
interval = 0.02 # the interval which we wait for a response
491-
end_time = time.time() + timeout
492488

493-
# run until the the future completes or the timeout elapses
494-
while block or (time.time() < end_time):
489+
# run until the the future arrives
490+
while True:
495491

496492
# block for a brief period to see if the future completes
497493
try:
@@ -503,26 +499,75 @@ def await_future(future, block=True, timeout=1.0):
503499
#
504500

505501
except Queue.Empty as e:
506-
logger.debug("Flushing future...")
502+
pass
503+
504+
logger.debug("Awaiting future...")
505+
506+
#
507+
# if we are executing (well, blocking) as the main thread, we need
508+
# to flush the event loop so IDA does not hang
509+
#
510+
511+
if idaapi.is_main_thread():
512+
flush_ida_sync_requests()
513+
514+
def await_lock(lock):
515+
"""
516+
Attempt to acquire a lock without blocking the IDA mainthread.
517+
518+
See await_future() for more details.
519+
"""
520+
521+
elapsed = 0 # total time elapsed waiting for the lock
522+
interval = 0.02 # the interval (in seconds) between acquire attempts
523+
timeout = 60.0 # the total time allotted to acquiring the lock
524+
end_time = time.time() + timeout
525+
526+
# wait until the the lock is available
527+
while time.time() < end_time:
528+
529+
#
530+
# attempt to acquire the given lock without blocking (via 'False').
531+
# if we succesfully aquire the lock, then we can return (success)
532+
#
533+
534+
if lock.acquire(False):
535+
logger.debug("Acquired lock!")
536+
return
537+
538+
#
539+
# the lock is not available yet. we need to sleep so we don't choke
540+
# the cpu, and try to acquire the lock again next time through...
541+
#
542+
543+
logger.debug("Awaiting lock...")
544+
time.sleep(interval)
545+
546+
#
547+
# if we are executing (well, blocking) as the main thread, we need
548+
# to flush the event loop so IDA does not hang
549+
#
550+
551+
if idaapi.is_main_thread():
507552
flush_ida_sync_requests()
508553

554+
#
555+
# we spent 60 seconds trying to acquire the lock, but never got it...
556+
# to avoid hanging IDA indefinitely (or worse), we abort via signal
557+
#
558+
559+
raise RuntimeError("Failed to acquire lock after %f seconds!" % timeout)
560+
509561
@mainthread
510562
def flush_ida_sync_requests():
511563
"""
512564
Flush all execute_sync requests.
513-
514-
NOTE: This MUST be called from the IDA Mainthread to be effective.
515565
"""
516-
if not idaapi.is_main_thread():
517-
return False
518566

519567
# this will trigger/flush the IDA UI loop
520568
qta = QtCore.QCoreApplication.instance()
521569
qta.processEvents()
522570

523-
# done
524-
return True
525-
526571
@mainthread
527572
def prompt_string(label, title, default=""):
528573
"""

0 commit comments

Comments
 (0)