[FIX] tools: make module import order consistent

A long time ago, in a commit far far away: 65a3751613,
we introduced a mechanism to support the "import emulator" feature[^1] of
cPython. It's used by some C native modules to trigger an on-the-spot
module import during a low-level function call.

Some of the modules that do this are `datetime` and `time`:
- https://github.com/python/cpython/blob/b6535ea7ecf40c51a9a13089d961c29abeaffa5e/Modules/timemodule.c#L892
- https://github.com/python/cpython/blob/b6535ea7ecf40c51a9a13089d961c29abeaffa5e/Modules/_datetimemodule.c#L1654

Those `PyImport_ImportModuleNoBlock()` calls were initially understood as real
calls that needed to return the imported module. But the actual return value is
never used by PyImport_Import. As long as the call succeeds, it is
sufficient that the modules are imported and present in `sys.modules`
afterwards:
https://github.com/python/cpython/blob/b6535ea7ecf40c51a9a13089d961c29abeaffa5e/Python/import.c#L1825-L1834

In order to avoid randomizing the order of module imports, which can cause
unpredictable behavior depending on the timing of those "emulated imports",
it's simpler to provide a "no-op" import function, and ensure that all the
allowed modules are imported in advance, when the server starts.

This is what this commit does. For good measure, it will raise an error
if the module being imported is not already imported, which means it was
not one of the expected/allowed modules. In that case, it's sufficient
to pre-import the module.

[^1]: https://github.com/python/cpython/blob/b6535ea7ecf40c51a9a13089d961c29abeaffa5e/Python/import.c#L1756-L1766

opw-3721469
This commit is contained in:
Olivier Dony
2024-02-07 13:48:01 +00:00
parent 3eddcee5c1
commit 4177e03c2d
+12 -10
View File
@@ -17,6 +17,7 @@ condition/math builtins.
import dis
import functools
import logging
import sys
import types
from opcode import HAVE_ARGUMENT, opmap, opname
from types import CodeType
@@ -37,6 +38,17 @@ __all__ = ['test_expr', 'safe_eval', 'const_eval']
# lp:703841), does import time.
_ALLOWED_MODULES = ['_strptime', 'math', 'time']
# Mock __import__ function, as called by cpython's import emulator `PyImport_Import` inside
# timemodule.c, _datetimemodule.c and others.
# This function does not actually need to do anything, its expected side-effect is to make the
# imported module available in `sys.modules`. The _ALLOWED_MODULES are imported below to make it so.
def _import(name, globals=None, locals=None, fromlist=None, level=-1):
if name not in sys.modules:
raise ImportError(f'module {name} should be imported before calling safe_eval()')
for module in _ALLOWED_MODULES:
__import__(module)
_UNSAFE_ATTRIBUTES = ['f_builtins', 'f_globals', 'f_locals', 'gi_frame', 'gi_code',
'co_code', 'func_globals']
@@ -264,16 +276,6 @@ def expr_eval(expr):
c = test_expr(expr, _EXPR_OPCODES)
return unsafe_eval(c)
def _import(name, globals=None, locals=None, fromlist=None, level=-1):
if globals is None:
globals = {}
if locals is None:
locals = {}
if fromlist is None:
fromlist = []
if name in _ALLOWED_MODULES:
return __import__(name, globals, locals, level)
raise ImportError(name)
_BUILTINS = {
'__import__': _import,
'True': True,