Skip to content

Commit 6a91668

Browse files
committed
gh-92041: Verify frame ownership and retain safe cached module hits
Preflight the ownership-only revision before updating #160254. Keep source retrieval unchanged and preserve linecache.getlines overrides. Developed and tested with Codex (AI assistance).
1 parent ef27738 commit 6a91668

4 files changed

Lines changed: 277 additions & 58 deletions

File tree

‎Doc/library/inspect.rst‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -772,14 +772,10 @@ Retrieving source code
772772
if the module cannot be determined.
773773

774774
.. versionchanged:: 3.16
775-
On a filename-cache miss, frames and tracebacks may be resolved from
776-
a registered module whose global namespace and canonical filename match
777-
the frame. Existing filename-cache entries retain precedence. If multiple
778-
modules share a filename, the result may differ from earlier Python
779-
versions and can depend on previous lookups: the frame's globals owner
780-
is not guaranteed to take precedence. Code objects continue to use
781-
filename-based lookup and can also be affected by earlier frame or
782-
traceback lookups.
775+
Frames and tracebacks are first matched to a module in :data:`sys.modules`
776+
using the frame's global namespace and normalized filename.
777+
Code objects still use filename-based lookup, which can identify a
778+
different module when multiple modules share a filename.
783779

784780

785781
.. function:: getsourcefile(object)

‎Lib/inspect.py‎

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -929,32 +929,93 @@ def getabsfile(object, _filename=None):
929929
modulesbyfile = {}
930930
_filesbymodname = {}
931931

932-
def _getframemodule(frame, filename):
933-
"""Resolve a frame's registered globals owner with a matching origin."""
932+
def _getframemodule(frame, explicit_filename):
933+
"""Return a matching globals owner and a reusable fallback filename.
934+
935+
The filename is only returned if the owner does not match and the
936+
filename-based lookup can reuse the normalized frame filename.
937+
"""
934938
namespace = frame.f_globals
935-
name = namespace.get('__name__')
939+
name = dict.get(namespace, '__name__')
936940
if type(name) is not str:
937-
return None
941+
return None, None
938942
module = sys.modules.get(name)
939-
# Leave module subclasses and dynamically supplied filenames to the fallback.
943+
# Leave subclasses and dynamic filenames to the existing lookup.
940944
if (type(module) is not types.ModuleType
941-
or module.__dict__ is not namespace
942-
or type(namespace.get('__file__')) is not str):
943-
return None
945+
or module.__dict__ is not namespace):
946+
return None, None
947+
module_filename = namespace.get('__file__')
948+
filename = frame.f_code.co_filename
949+
# Avoid invoking user-defined comparisons when matching filenames.
950+
if type(module_filename) is not str or type(filename) is not str:
951+
return None, None
952+
if explicit_filename is not None and explicit_filename != filename:
953+
return None, None
954+
# Equal filenames need no normalization. An explicit bytecode
955+
# filename must retain the source/bytecode distinction.
956+
if module_filename == filename and (
957+
explicit_filename is None or not filename.endswith(
958+
tuple(importlib.machinery.BYTECODE_SUFFIXES))):
959+
return module, None
960+
bytecode_suffixes = tuple(importlib.machinery.BYTECODE_SUFFIXES)
961+
is_bytecode = filename.endswith(bytecode_suffixes)
944962
try:
945-
if filename == getabsfile(module):
946-
return module
947-
except FileNotFoundError, TypeError:
963+
# Resolve bytecode aliases using the owner's loader to avoid
964+
# looking up the frame recursively through getsourcefile().
965+
if explicit_filename is None and is_bytecode:
966+
source = (os.path.splitext(filename)[0]
967+
+ importlib.machinery.SOURCE_SUFFIXES[0])
968+
if (source in linecache.cache or os.path.exists(source)
969+
or getattr(module, '__loader__', None) is not None
970+
or getattr(getattr(module, '__spec__', None),
971+
'loader', None) is not None):
972+
filename = source
973+
if module_filename.endswith(bytecode_suffixes):
974+
source = (os.path.splitext(module_filename)[0]
975+
+ importlib.machinery.SOURCE_SUFFIXES[0])
976+
if source == filename and namespace.get('__loader__') is not None:
977+
return module, None
978+
origin = getabsfile(module)
979+
else:
980+
origin = getabsfile(module, module_filename)
981+
file = getabsfile(frame, filename)
982+
if origin == file:
983+
return module, None
984+
# Reuse the path only if it preserves explicit-filename
985+
# precedence and bytecode source resolution.
986+
if explicit_filename is None and not is_bytecode:
987+
return None, file
988+
except (FileNotFoundError, TypeError):
948989
pass
949-
return None
990+
return None, None
950991

951992

952993
def getmodule(object, _filename=None):
953994
"""Return the module an object was defined in, or None if not found."""
954-
if ismodule(object):
955-
return object
956-
if hasattr(object, '__module__'):
957-
return sys.modules.get(object.__module__)
995+
object_type = type(object)
996+
if object_type is types.FrameType:
997+
frame = object
998+
elif object_type is types.TracebackType:
999+
frame = object.tb_frame
1000+
else:
1001+
if isinstance(object, types.ModuleType):
1002+
return object
1003+
if hasattr(object, '__module__'):
1004+
return sys.modules.get(object.__module__)
1005+
frame = None
1006+
if frame is not None:
1007+
# A module with these exact globals is the only possible globals
1008+
# owner. Otherwise verify ownership before using a cached result.
1009+
if _filename is not None and _filename in modulesbyfile:
1010+
module = sys.modules.get(modulesbyfile[_filename])
1011+
if (type(module) is types.ModuleType
1012+
and module.__dict__ is frame.f_globals):
1013+
return module
1014+
module, fallback_filename = _getframemodule(frame, _filename)
1015+
if module is not None:
1016+
return module
1017+
if fallback_filename is not None:
1018+
_filename = fallback_filename
9581019

9591020
# Try the filename to modulename cache
9601021
if _filename is not None and _filename in modulesbyfile:
@@ -966,19 +1027,6 @@ def getmodule(object, _filename=None):
9661027
return None
9671028
if file in modulesbyfile:
9681029
return sys.modules.get(modulesbyfile[file])
969-
if isframe(object):
970-
frame = object
971-
elif istraceback(object):
972-
frame = object.tb_frame
973-
else:
974-
frame = None
975-
if frame is not None and (
976-
_filename is None or _filename == frame.f_code.co_filename):
977-
module = _getframemodule(frame, file)
978-
if module is not None:
979-
modulesbyfile[file] = module.__name__
980-
return module
981-
9821030
# Update the filename to module name cache and check yet again
9831031
# Copy sys.modules in order to cope with changes while iterating
9841032
for modname, module in sys.modules.copy().items():

0 commit comments

Comments
 (0)