@@ -2048,6 +2048,58 @@ def find_spec(
20482048 assert not marker .exists ()
20492049
20502050
2051+ def test_call_graph_fails_closed_for_meta_path_finder_installed_before_import (tmp_path : Path ) -> None :
2052+ module_dir = tmp_path / "modules"
2053+ module_dir .mkdir ()
2054+ module_name = "modelaudit_tp_preimport_meta_path_probe"
2055+ (module_dir / f"{ module_name } .py" ).write_text (
2056+ "def invoke(command):\n return command\n " ,
2057+ encoding = "utf-8" ,
2058+ )
2059+ marker = tmp_path / "preimport_meta_path_called"
2060+ child_code = """
2061+ import sys
2062+ from importlib.machinery import ModuleSpec
2063+ from pathlib import Path
2064+
2065+ module_dir = Path(sys.argv[1])
2066+ marker = Path(sys.argv[2])
2067+ module_name = sys.argv[3]
2068+
2069+ class PreexistingMetaPathFinder:
2070+ @staticmethod
2071+ def find_spec(fullname, path=None, target=None):
2072+ del path, target
2073+ if fullname == module_name:
2074+ marker.write_text(fullname, encoding="utf-8")
2075+ return ModuleSpec(fullname, loader=None, origin="custom://module")
2076+ return None
2077+
2078+ sys.path.insert(0, str(module_dir))
2079+ sys.meta_path.insert(0, PreexistingMetaPathFinder())
2080+ from modelaudit_picklescan.call_graph import _call_graph_source_unavailable_reason
2081+
2082+ reason = _call_graph_source_unavailable_reason(module_name)
2083+ if reason != "source_unavailable":
2084+ raise SystemExit(f"unexpected source reason: {reason!r}")
2085+ if marker.exists():
2086+ raise SystemExit("pre-existing finder was invoked")
2087+ """
2088+
2089+ result = subprocess .run (
2090+ [sys .executable , "-c" , child_code , str (module_dir ), str (marker ), module_name ],
2091+ cwd = str (tmp_path ),
2092+ env = dict (os .environ ),
2093+ check = False ,
2094+ capture_output = True ,
2095+ text = True ,
2096+ timeout = 30 ,
2097+ )
2098+
2099+ assert result .returncode == 0 , result .stderr
2100+ assert not marker .exists ()
2101+
2102+
20512103def test_scan_bytes_analyzes_stdlib_source_modules_without_custom_meta_path_finders (
20522104 tmp_path : Path ,
20532105 monkeypatch : pytest .MonkeyPatch ,
@@ -2305,6 +2357,84 @@ def custom_path_hook(path: str) -> object:
23052357 assert not marker .exists ()
23062358
23072359
2360+ def test_call_graph_rejects_path_hook_with_spoofed_standard_metadata (
2361+ tmp_path : Path ,
2362+ monkeypatch : pytest .MonkeyPatch ,
2363+ ) -> None :
2364+ module_name = "modelaudit_tp_spoofed_path_hook_probe"
2365+ module_dir = tmp_path / "modules"
2366+ module_dir .mkdir ()
2367+ (module_dir / f"{ module_name } .py" ).write_text ("def invoke():\n return None\n " , encoding = "utf-8" )
2368+ marker = tmp_path / "spoofed_path_hook_called"
2369+
2370+ def spoofed_path_hook (path : str ) -> object :
2371+ marker .write_text (path , encoding = "utf-8" )
2372+ raise ImportError
2373+
2374+ spoofed_path_hook .__module__ = "_frozen_importlib_external"
2375+ spoofed_path_hook .__qualname__ = "FileFinder.path_hook.<locals>.path_hook_for_FileFinder"
2376+ monkeypatch .syspath_prepend (str (module_dir ))
2377+ monkeypatch .setattr (sys , "path_hooks" , [spoofed_path_hook , * sys .path_hooks ])
2378+ sys .path_importer_cache .pop (str (module_dir ), None )
2379+ _clear_call_graph_caches ()
2380+
2381+ try :
2382+ assert call_graph ._call_graph_source_unavailable_reason (module_name ) == "source_unavailable"
2383+ finally :
2384+ _clear_call_graph_caches ()
2385+
2386+ assert not marker .exists ()
2387+
2388+
2389+ def test_call_graph_honors_zipimport_precedence_over_later_extension (
2390+ tmp_path : Path ,
2391+ monkeypatch : pytest .MonkeyPatch ,
2392+ ) -> None :
2393+ module_name = "modelaudit_tp_zip_shadowing_extension"
2394+ zip_path = tmp_path / "modules.zip"
2395+ extension_dir = tmp_path / "extension"
2396+ extension_dir .mkdir ()
2397+ with zipfile .ZipFile (zip_path , "w" ) as archive :
2398+ archive .writestr (f"{ module_name } .py" , "def invoke(command):\n return command\n " )
2399+ (extension_dir / f"{ module_name } { EXTENSION_SUFFIXES [0 ]} " ).write_bytes (b"not imported" )
2400+ monkeypatch .setattr (sys , "path" , [str (zip_path ), str (extension_dir ), * sys .path ])
2401+ importlib .invalidate_caches ()
2402+ _clear_call_graph_caches ()
2403+
2404+ try :
2405+ report = scan_bytes (
2406+ _global_call_payload (module_name , "invoke" , _unicode_operand ("echo hidden" )),
2407+ source = "zip-shadowing-extension.pkl" ,
2408+ )
2409+ finally :
2410+ _clear_call_graph_caches ()
2411+
2412+ assert report .status == ScanStatus .INCONCLUSIVE
2413+ assert report .verdict == SafetyVerdict .UNKNOWN
2414+ assert _has_call_graph_source_unavailable_notice (report , module_name , "invoke" , "source_unavailable" )
2415+
2416+
2417+ def test_call_graph_keeps_extension_precedence_over_later_zipimport (
2418+ tmp_path : Path ,
2419+ monkeypatch : pytest .MonkeyPatch ,
2420+ ) -> None :
2421+ module_name = "modelaudit_tp_extension_shadowing_zip"
2422+ extension_dir = tmp_path / "extension"
2423+ extension_dir .mkdir ()
2424+ zip_path = tmp_path / "modules.zip"
2425+ (extension_dir / f"{ module_name } { EXTENSION_SUFFIXES [0 ]} " ).write_bytes (b"not imported" )
2426+ with zipfile .ZipFile (zip_path , "w" ) as archive :
2427+ archive .writestr (f"{ module_name } .py" , "def invoke(command):\n return command\n " )
2428+ monkeypatch .setattr (sys , "path" , [str (extension_dir ), str (zip_path ), * sys .path ])
2429+ importlib .invalidate_caches ()
2430+ _clear_call_graph_caches ()
2431+
2432+ try :
2433+ assert call_graph ._call_graph_source_unavailable_reason (module_name ) is None
2434+ finally :
2435+ _clear_call_graph_caches ()
2436+
2437+
23082438def test_scan_bytes_marks_bytecode_only_invoked_call_graph_source_unavailable (
23092439 tmp_path : Path ,
23102440 monkeypatch : pytest .MonkeyPatch ,
@@ -2614,6 +2744,7 @@ def test_scan_bytes_analyzes_shadowed_torch_extension_callable_invocation(
26142744 "import os\n \n def device(command):\n os.system(command)\n return command\n " ,
26152745 encoding = "utf-8" ,
26162746 )
2747+ monkeypatch .delitem (sys .modules , "torch" , raising = False )
26172748 monkeypatch .syspath_prepend (str (module_dir ))
26182749 importlib .invalidate_caches ()
26192750 _clear_call_graph_caches ()
@@ -2658,6 +2789,7 @@ def __call__(self, command):
26582789""" ,
26592790 encoding = "utf-8" ,
26602791 )
2792+ monkeypatch .delitem (sys .modules , "torch" , raising = False )
26612793 monkeypatch .syspath_prepend (str (module_dir ))
26622794 importlib .invalidate_caches ()
26632795 _clear_call_graph_caches ()
@@ -2745,6 +2877,7 @@ def test_call_graph_analyzes_shadowed_torch_storage_persistent_id_reference(
27452877 "import os\n \n def __getattr__(name):\n os.system(name)\n raise AttributeError(name)\n " ,
27462878 encoding = "utf-8" ,
27472879 )
2880+ monkeypatch .delitem (sys .modules , "torch" , raising = False )
27482881 monkeypatch .syspath_prepend (str (module_dir ))
27492882 importlib .invalidate_caches ()
27502883 _clear_call_graph_caches ()
0 commit comments