Skip to content

fix(moduleloader): isolate module namespaces and cache loaded modules#1132

Open
Marshall-Hallenbeck wants to merge 1 commit intomainfrom
fix/moduleloader-isolated-namespaces
Open

fix(moduleloader): isolate module namespaces and cache loaded modules#1132
Marshall-Hallenbeck wants to merge 1 commit intomainfrom
fix/moduleloader-isolated-namespaces

Conversation

@Marshall-Hallenbeck
Copy link
Collaborator

Description

The module loader uses spec_from_file_location("NXCModule", module_path) with the same module name "NXCModule" for every module file. When multiple modules are loaded via -M, each subsequent module overwrites the NXCModule class in the shared sys.modules["NXCModule"] namespace. This means modules loaded earlier resolve a different module's NXCModule class when referencing it — causing attribute errors, wrong method calls, or other subtle breakage depending on which module loads last.

This is the underlying mechanism behind the multi-module ordering bugs reported in #879, #880, and #882 — all of which describe symptoms where the order of -M flags changes behavior or causes failures.

Changes:

  1. Isolated namespaces — Each module file now gets a unique name (nxc_module_<filename>) and is loaded via the modern module_from_spec() + exec_module() API instead of the deprecated load_module(). This gives each module its own namespace — no more cross-module pollution.

  2. Module caching — Loaded modules are cached in a class-level dict so each file is only parsed and executed once, regardless of how many targets are scanned. This is ~60x faster than the old code for multi-target scans:

    Approach 6250 loads (50 targets × 125 modules) Per load
    Old code (load_module with sys.modules) 1.806s 0.29ms
    New code (cached exec_module) 0.029s ~0.005ms
  3. DRY — Extracted shared loading logic into load_module_file() classmethod, used by both load_module() and get_module_info().

AI disclosure: Claude Code (Claude Opus 4.6) was used to assist with root cause analysis, benchmarking, and drafting the fix. The bug was discovered during a real pentest scan, root cause was traced and verified by human and AI together, and the fix was human-reviewed and tested on live targets.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Deprecation of feature or functionality
  • This change requires a documentation update
  • This requires a third party update (such as Impacket, Dploot, lsassy, etc)
  • This PR was created with the assistance of AI (list what type of assistance, tool(s)/model(s) in the description)

Setup guide for the review

How to demonstrate the namespace collision (without this fix):

import importlib.util, sys
spec1 = importlib.util.spec_from_file_location("NXCModule", "nxc/modules/coerce_plus.py")
mod1 = spec1.loader.load_module()
print(hasattr(mod1.NXCModule, "get_dynamic_endpoint"))  # True

spec2 = importlib.util.spec_from_file_location("NXCModule", "nxc/modules/zerologon.py")
mod2 = spec2.loader.load_module()

# NXCModule in the shared namespace now points to zerologon's class
resolved = sys.modules["NXCModule"].__dict__["NXCModule"]
print(resolved.name)  # "zerologon" — not coerce_plus!
print(hasattr(resolved, "get_dynamic_endpoint"))  # False — clobbered

Tested on:

  • Kali Linux 6.17.10+kali-amd64, Python 3.13.9
  • Targets: Windows Server 2016/2019, Windows 10 Build 17763/19041
  • All 144 unit tests pass

Screenshots (if appropriate):

N/A — this is an internal loader change with no user-visible output difference (other than fixing the errors).

Checklist:

  • I have ran Ruff against my changes (poetry: poetry run ruff check ., use --fix to automatically fix what it can)
  • I have added or updated the tests/e2e_commands.txt file if necessary (new modules or features are required to be added to the e2e tests)
  • If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
  • I have linked relevant sources that describes the added technique (blog posts, documentation, etc)
  • I have performed a self-review of my own code (not an AI review)
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)

The module loader used the same module name "NXCModule" for every file
via spec_from_file_location(), causing sys.modules namespace collisions
when multiple modules are loaded together (-M mod1 -M mod2). Each
subsequent module overwrites the NXCModule class in the shared namespace,
so modules loaded earlier resolve a different class than their own.

This is the underlying mechanism behind multi-module ordering bugs
reported in #879, #880, and #882.

Changes:
- Use unique module names (nxc_module_<filename>) and the modern
  module_from_spec() + exec_module() API instead of the deprecated
  load_module() to give each module its own isolated namespace
- Cache loaded modules so each file is only parsed and executed once,
  regardless of how many targets are scanned (~60x faster than
  re-loading per target)
- Extract shared loading logic into load_module_file() classmethod

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes multi-module ordering bugs by ensuring each loaded module file gets an isolated import namespace (avoiding sys.modules["NXCModule"] collisions) and by caching loaded module files to avoid repeated parsing/execution across many targets.

Changes:

  • Introduces a class-level module cache and a shared load_module_file() loader using module_from_spec() + exec_module().
  • Updates load_module() and get_module_info() to use the shared loader (and benefit from isolation + caching).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Marshall-Hallenbeck Marshall-Hallenbeck self-assigned this Mar 6, 2026
@Marshall-Hallenbeck Marshall-Hallenbeck added enhancement New feature or request bug-fix This Pull Request fixes a bug performance labels Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug-fix This Pull Request fixes a bug enhancement New feature or request performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants