- 
          
- 
                Notifications
    You must be signed in to change notification settings 
- Fork 928
fix multi-threading issue #1146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -7,6 +7,7 @@ | |
|  | ||
| from nettacker import logger | ||
| from nettacker.config import Config | ||
| from nettacker.core import queue_manager | ||
| from nettacker.core.messages import messages as _ | ||
| from nettacker.core.template import TemplateLoader | ||
| from nettacker.core.utils.common import expand_module_steps, wait_for_threads_to_finish | ||
|  | @@ -118,29 +119,48 @@ def generate_loops(self): | |
| self.module_content["payloads"] = expand_module_steps(self.module_content["payloads"]) | ||
|  | ||
| def sort_loops(self): | ||
| steps = [] | ||
| """ | ||
| Sort loops to optimize dependency resolution: | ||
| 1. Independent steps first | ||
| 2. Steps that generate dependencies (save_to_temp_events_only) | ||
| 3. Steps that consume dependencies (dependent_on_temp_event) | ||
| """ | ||
| for index in range(len(self.module_content["payloads"])): | ||
| for step in copy.deepcopy(self.module_content["payloads"][index]["steps"]): | ||
| if "dependent_on_temp_event" not in step[0]["response"]: | ||
| steps.append(step) | ||
| independent_steps = [] | ||
| dependency_generators = [] | ||
| dependency_consumers = [] | ||
|  | ||
| for step in copy.deepcopy(self.module_content["payloads"][index]["steps"]): | ||
| if ( | ||
| "dependent_on_temp_event" in step[0]["response"] | ||
| and "save_to_temp_events_only" in step[0]["response"] | ||
| ): | ||
| steps.append(step) | ||
| step_response = step[0]["response"] if step and len(step) > 0 else {} | ||
|  | ||
| for step in copy.deepcopy(self.module_content["payloads"][index]["steps"]): | ||
| if ( | ||
| "dependent_on_temp_event" in step[0]["response"] | ||
| and "save_to_temp_events_only" not in step[0]["response"] | ||
| ): | ||
| steps.append(step) | ||
| self.module_content["payloads"][index]["steps"] = steps | ||
| has_dependency = "dependent_on_temp_event" in step_response | ||
| generates_dependency = "save_to_temp_events_only" in step_response | ||
|  | ||
| if not has_dependency and not generates_dependency: | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is equally hard to read, maybe something like this would be better? no_dep = []
dep_temp_only = []
dep_normal = []
for step in copy.deepcopy(self.module_content["payloads"][index]["steps"])::
    resp = step[0]["response"]
    if "dependent_on_temp_event" not in resp:
        no_dep.append(step)
    elif "save_to_temp_events_only" in resp:
        dep_temp_only.append(step)
    else:
        dep_normal.append(step)
payload["steps"] = no_dep + dep_temp_only + dep_normalThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this change would benefit apart from this PR as well. Maybe you can make a new one with this, because the current one is O(n) but running it thrice with three deepcopies is bad. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay will fix it, is it okay if we add this changes in new commit in this pr ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can make a separate one with this because it will probably be a while before this one is tested nicely, and this change is easier to verify and test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. | ||
| independent_steps.append(step) | ||
| elif generates_dependency and not has_dependency: | ||
| dependency_generators.append(step) | ||
| elif generates_dependency and has_dependency: | ||
| dependency_generators.append(step) # Generator first | ||
| elif has_dependency and not generates_dependency: | ||
| dependency_consumers.append(step) | ||
| else: | ||
| independent_steps.append(step) # Fallback | ||
|  | ||
| # Combine in optimal order | ||
| sorted_steps = independent_steps + dependency_generators + dependency_consumers | ||
| self.module_content["payloads"][index]["steps"] = sorted_steps | ||
|  | ||
| log.verbose_info( | ||
| f"Sorted {len(sorted_steps)} steps: " | ||
| f"{len(independent_steps)} independent, " | ||
| f"{len(dependency_generators)} generators, " | ||
| f"{len(dependency_consumers)} consumers" | ||
| ) | ||
|  | ||
| def start(self): | ||
| active_threads = [] | ||
| used_shared_pool = False | ||
|  | ||
| # counting total number of requests | ||
| total_number_of_requests = 0 | ||
|  | @@ -158,11 +178,16 @@ def start(self): | |
| importlib.import_module(f"nettacker.core.lib.{library.lower()}"), | ||
| f"{library.capitalize()}Engine", | ||
| )() | ||
|  | ||
| for step in payload["steps"]: | ||
| for sub_step in step: | ||
| thread = Thread( | ||
| target=engine.run, | ||
| args=( | ||
| # Try to use shared thread pool if available, otherwise use local threads | ||
| if queue_manager.thread_pool and hasattr( | ||
| queue_manager.thread_pool, "submit_task" | ||
| ): | ||
| # Submit to shared thread pool | ||
| queue_manager.thread_pool.submit_task( | ||
| engine.run, | ||
| sub_step, | ||
| self.module_name, | ||
| self.target, | ||
|  | @@ -173,9 +198,36 @@ def start(self): | |
| self.total_module_thread_number, | ||
| request_number_counter, | ||
| total_number_of_requests, | ||
| ), | ||
| ) | ||
| thread.name = f"{self.target} -> {self.module_name} -> {sub_step}" | ||
| ) | ||
| used_shared_pool = True | ||
| else: | ||
| # Use local thread (fallback to original behavior) | ||
| thread = Thread( | ||
| target=engine.run, | ||
| args=( | ||
| sub_step, | ||
| self.module_name, | ||
| self.target, | ||
| self.scan_id, | ||
| self.module_inputs, | ||
| self.process_number, | ||
| self.module_thread_number, | ||
| self.total_module_thread_number, | ||
| request_number_counter, | ||
| total_number_of_requests, | ||
| ), | ||
| ) | ||
| thread.name = f"{self.target} -> {self.module_name} -> {sub_step}" | ||
| thread.start() | ||
| active_threads.append(thread) | ||
|  | ||
| # Manage local thread pool size | ||
| wait_for_threads_to_finish( | ||
| active_threads, | ||
| maximum=self.module_inputs["thread_per_host"], | ||
| terminable=True, | ||
| ) | ||
|  | ||
| request_number_counter += 1 | ||
| log.verbose_event_info( | ||
| _("sending_module_request").format( | ||
|  | @@ -188,13 +240,19 @@ def start(self): | |
| total_number_of_requests, | ||
| ) | ||
| ) | ||
| thread.start() | ||
| time.sleep(self.module_inputs["time_sleep_between_requests"]) | ||
| active_threads.append(thread) | ||
| wait_for_threads_to_finish( | ||
| active_threads, | ||
| maximum=self.module_inputs["thread_per_host"], | ||
| terminable=True, | ||
| ) | ||
|  | ||
| wait_for_threads_to_finish(active_threads, maximum=None, terminable=True) | ||
| # Wait for completion based on execution path | ||
| if used_shared_pool: | ||
| # Wait for shared thread pool tasks to complete | ||
| if queue_manager.thread_pool and hasattr( | ||
| queue_manager.thread_pool, "wait_for_completion" | ||
| ): | ||
| # Wait with a reasonable timeout to prevent hanging | ||
| completed = queue_manager.thread_pool.wait_for_completion(timeout=300) # 5 minutes | ||
| if not completed: | ||
| log.warn(f"Module {self.module_name} tasks did not complete within timeout") | ||
|  | ||
| # Wait for any remaining local threads to finish | ||
| if active_threads: | ||
| wait_for_threads_to_finish(active_threads, maximum=None, terminable=True) | ||
|         
                  Davda-James marked this conversation as resolved.
              Show resolved
            Hide resolved 
      Comment on lines
    
      +245
     to 
      +258
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Waits for global pool completion, not this module’s submissions 
 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you try and quantify as well how effective using this is? I understand theoretically, polling every 100ms is not the best approach but this function is only called under the condition that
dependent_on_temp_eventis present in the response (that is for a few vuln and scan modules), for HTTP implementation.So I am assuming that this polling doesn't really have to run for long periods. Given that, if you can justify adding this complexity, it would be cool.