mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	gh-98627: Add an Optional Check for Extension Module Subinterpreter Compatibility (gh-99040)
Enforcing (optionally) the restriction set by PEP 489 makes sense. Furthermore, this sets the stage for a potential restriction related to a per-interpreter GIL. This change includes the following: * add tests for extension module subinterpreter compatibility * add _PyInterpreterConfig.check_multi_interp_extensions * add Py_RTFLAGS_MULTI_INTERP_EXTENSIONS * add _PyImport_CheckSubinterpIncompatibleExtensionAllowed() * fail iff the module does not implement multi-phase init and the current interpreter is configured to check https://github.com/python/cpython/issues/98627
This commit is contained in:
		
							parent
							
								
									3dea4ba6c1
								
							
						
					
					
						commit
						89ac665891
					
				
					 15 changed files with 557 additions and 19 deletions
				
			
		|  | @ -248,6 +248,7 @@ typedef struct { | |||
|     int allow_exec; | ||||
|     int allow_threads; | ||||
|     int allow_daemon_threads; | ||||
|     int check_multi_interp_extensions; | ||||
| } _PyInterpreterConfig; | ||||
| 
 | ||||
| #define _PyInterpreterConfig_INIT \ | ||||
|  | @ -256,6 +257,7 @@ typedef struct { | |||
|         .allow_exec = 0, \ | ||||
|         .allow_threads = 1, \ | ||||
|         .allow_daemon_threads = 0, \ | ||||
|         .check_multi_interp_extensions = 1, \ | ||||
|     } | ||||
| 
 | ||||
| #define _PyInterpreterConfig_LEGACY_INIT \ | ||||
|  | @ -264,6 +266,7 @@ typedef struct { | |||
|         .allow_exec = 1, \ | ||||
|         .allow_threads = 1, \ | ||||
|         .allow_daemon_threads = 1, \ | ||||
|         .check_multi_interp_extensions = 0, \ | ||||
|     } | ||||
| 
 | ||||
| /* --- Helper functions --------------------------------------- */ | ||||
|  |  | |||
|  | @ -11,6 +11,9 @@ is available in a given context.  For example, forking the process | |||
| might not be allowed in the current interpreter (i.e. os.fork() would fail). | ||||
| */ | ||||
| 
 | ||||
| /* Set if import should check a module for subinterpreter support. */ | ||||
| #define Py_RTFLAGS_MULTI_INTERP_EXTENSIONS (1UL << 8) | ||||
| 
 | ||||
| /* Set if threads are allowed. */ | ||||
| #define Py_RTFLAGS_THREADS (1UL << 10) | ||||
| 
 | ||||
|  |  | |||
|  | @ -64,6 +64,7 @@ struct _import_state { | |||
|     /* override for config->use_frozen_modules (for tests)
 | ||||
|        (-1: "off", 1: "on", 0: no override) */ | ||||
|     int override_frozen_modules; | ||||
|     int override_multi_interp_extensions_check; | ||||
| #ifdef HAVE_DLOPEN | ||||
|     int dlopenflags; | ||||
| #endif | ||||
|  | @ -153,6 +154,10 @@ PyAPI_DATA(const struct _frozen *) _PyImport_FrozenStdlib; | |||
| PyAPI_DATA(const struct _frozen *) _PyImport_FrozenTest; | ||||
| extern const struct _module_alias * _PyImport_FrozenAliases; | ||||
| 
 | ||||
| PyAPI_FUNC(int) _PyImport_CheckSubinterpIncompatibleExtensionAllowed( | ||||
|     const char *name); | ||||
| 
 | ||||
| 
 | ||||
| // for testing
 | ||||
| PyAPI_FUNC(int) _PyImport_ClearExtension(PyObject *name, PyObject *filename); | ||||
| 
 | ||||
|  |  | |||
|  | @ -105,6 +105,24 @@ def frozen_modules(enabled=True): | |||
|         _imp._override_frozen_modules_for_tests(0) | ||||
| 
 | ||||
| 
 | ||||
| @contextlib.contextmanager | ||||
| def multi_interp_extensions_check(enabled=True): | ||||
|     """Force legacy modules to be allowed in subinterpreters (or not). | ||||
| 
 | ||||
|     ("legacy" == single-phase init) | ||||
| 
 | ||||
|     This only applies to modules that haven't been imported yet. | ||||
|     It overrides the PyInterpreterConfig.check_multi_interp_extensions | ||||
|     setting (see support.run_in_subinterp_with_config() and | ||||
|     _xxsubinterpreters.create()). | ||||
|     """ | ||||
|     old = _imp._override_multi_interp_extensions_check(1 if enabled else -1) | ||||
|     try: | ||||
|         yield | ||||
|     finally: | ||||
|         _imp._override_multi_interp_extensions_check(old) | ||||
| 
 | ||||
| 
 | ||||
| def import_fresh_module(name, fresh=(), blocked=(), *, | ||||
|                         deprecated=False, | ||||
|                         usefrozen=False, | ||||
|  |  | |||
							
								
								
									
										77
									
								
								Lib/test/test_capi/check_config.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								Lib/test/test_capi/check_config.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,77 @@ | |||
| # This script is used by test_misc. | ||||
| 
 | ||||
| import _imp | ||||
| import _testinternalcapi | ||||
| import json | ||||
| import os | ||||
| import sys | ||||
| 
 | ||||
| 
 | ||||
| def import_singlephase(): | ||||
|     assert '_testsinglephase' not in sys.modules | ||||
|     try: | ||||
|         import _testsinglephase | ||||
|     except ImportError: | ||||
|         sys.modules.pop('_testsinglephase') | ||||
|         return False | ||||
|     else: | ||||
|         del sys.modules['_testsinglephase'] | ||||
|         return True | ||||
| 
 | ||||
| 
 | ||||
| def check_singlephase(override): | ||||
|     # Check using the default setting. | ||||
|     settings_initial = _testinternalcapi.get_interp_settings() | ||||
|     allowed_initial = import_singlephase() | ||||
|     assert(_testinternalcapi.get_interp_settings() == settings_initial) | ||||
| 
 | ||||
|     # Apply the override and check. | ||||
|     override_initial = _imp._override_multi_interp_extensions_check(override) | ||||
|     settings_after = _testinternalcapi.get_interp_settings() | ||||
|     allowed_after = import_singlephase() | ||||
| 
 | ||||
|     # Apply the override again and check. | ||||
|     noop = {} | ||||
|     override_after = _imp._override_multi_interp_extensions_check(override) | ||||
|     settings_noop = _testinternalcapi.get_interp_settings() | ||||
|     if settings_noop != settings_after: | ||||
|         noop['settings_noop'] = settings_noop | ||||
|     allowed_noop = import_singlephase() | ||||
|     if allowed_noop != allowed_after: | ||||
|         noop['allowed_noop'] = allowed_noop | ||||
| 
 | ||||
|     # Restore the original setting and check. | ||||
|     override_noop = _imp._override_multi_interp_extensions_check(override_initial) | ||||
|     if override_noop != override_after: | ||||
|         noop['override_noop'] = override_noop | ||||
|     settings_restored = _testinternalcapi.get_interp_settings() | ||||
|     allowed_restored = import_singlephase() | ||||
| 
 | ||||
|     # Restore the original setting again. | ||||
|     override_restored = _imp._override_multi_interp_extensions_check(override_initial) | ||||
|     assert(_testinternalcapi.get_interp_settings() == settings_restored) | ||||
| 
 | ||||
|     return dict({ | ||||
|         'requested': override, | ||||
|         'override__initial': override_initial, | ||||
|         'override_after': override_after, | ||||
|         'override_restored': override_restored, | ||||
|         'settings__initial': settings_initial, | ||||
|         'settings_after': settings_after, | ||||
|         'settings_restored': settings_restored, | ||||
|         'allowed__initial': allowed_initial, | ||||
|         'allowed_after': allowed_after, | ||||
|         'allowed_restored': allowed_restored, | ||||
|     }, **noop) | ||||
| 
 | ||||
| 
 | ||||
| def run_singlephase_check(override, outfd): | ||||
|     with os.fdopen(outfd, 'w') as outfile: | ||||
|         sys.stdout = outfile | ||||
|         sys.stderr = outfile | ||||
|         try: | ||||
|             results = check_singlephase(override) | ||||
|             json.dump(results, outfile) | ||||
|         finally: | ||||
|             sys.stdout = sys.__stdout__ | ||||
|             sys.stderr = sys.__stderr__ | ||||
|  | @ -31,6 +31,10 @@ | |||
|     import _testmultiphase | ||||
| except ImportError: | ||||
|     _testmultiphase = None | ||||
| try: | ||||
|     import _testsinglephase | ||||
| except ImportError: | ||||
|     _testsinglephase = None | ||||
| 
 | ||||
| # Skip this test if the _testcapi module isn't available. | ||||
| _testcapi = import_helper.import_module('_testcapi') | ||||
|  | @ -1297,17 +1301,20 @@ def test_configured_settings(self): | |||
|         """ | ||||
|         import json | ||||
| 
 | ||||
|         EXTENSIONS = 1<<8 | ||||
|         THREADS = 1<<10 | ||||
|         DAEMON_THREADS = 1<<11 | ||||
|         FORK = 1<<15 | ||||
|         EXEC = 1<<16 | ||||
| 
 | ||||
|         features = ['fork', 'exec', 'threads', 'daemon_threads'] | ||||
|         features = ['fork', 'exec', 'threads', 'daemon_threads', 'extensions'] | ||||
|         kwlist = [f'allow_{n}' for n in features] | ||||
|         kwlist[-1] = 'check_multi_interp_extensions' | ||||
|         for config, expected in { | ||||
|             (True, True, True, True): FORK | EXEC | THREADS | DAEMON_THREADS, | ||||
|             (False, False, False, False): 0, | ||||
|             (False, False, True, False): THREADS, | ||||
|             (True, True, True, True, True): | ||||
|                 FORK | EXEC | THREADS | DAEMON_THREADS | EXTENSIONS, | ||||
|             (False, False, False, False, False): 0, | ||||
|             (False, False, True, False, True): THREADS | EXTENSIONS, | ||||
|         }.items(): | ||||
|             kwargs = dict(zip(kwlist, config)) | ||||
|             expected = { | ||||
|  | @ -1322,12 +1329,93 @@ def test_configured_settings(self): | |||
|                         json.dump(settings, stdin) | ||||
|                     ''') | ||||
|                 with os.fdopen(r) as stdout: | ||||
|                     support.run_in_subinterp_with_config(script, **kwargs) | ||||
|                     ret = support.run_in_subinterp_with_config(script, **kwargs) | ||||
|                     self.assertEqual(ret, 0) | ||||
|                     out = stdout.read() | ||||
|                 settings = json.loads(out) | ||||
| 
 | ||||
|                 self.assertEqual(settings, expected) | ||||
| 
 | ||||
|     @unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module") | ||||
|     @unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()") | ||||
|     def test_overridden_setting_extensions_subinterp_check(self): | ||||
|         """ | ||||
|         PyInterpreterConfig.check_multi_interp_extensions can be overridden | ||||
|         with PyInterpreterState.override_multi_interp_extensions_check. | ||||
|         This verifies that the override works but does not modify | ||||
|         the underlying setting. | ||||
|         """ | ||||
|         import json | ||||
| 
 | ||||
|         EXTENSIONS = 1<<8 | ||||
|         THREADS = 1<<10 | ||||
|         DAEMON_THREADS = 1<<11 | ||||
|         FORK = 1<<15 | ||||
|         EXEC = 1<<16 | ||||
|         BASE_FLAGS = FORK | EXEC | THREADS | DAEMON_THREADS | ||||
|         base_kwargs = { | ||||
|             'allow_fork': True, | ||||
|             'allow_exec': True, | ||||
|             'allow_threads': True, | ||||
|             'allow_daemon_threads': True, | ||||
|         } | ||||
| 
 | ||||
|         def check(enabled, override): | ||||
|             kwargs = dict( | ||||
|                 base_kwargs, | ||||
|                 check_multi_interp_extensions=enabled, | ||||
|             ) | ||||
|             flags = BASE_FLAGS | EXTENSIONS if enabled else BASE_FLAGS | ||||
|             settings = { | ||||
|                 'feature_flags': flags, | ||||
|             } | ||||
| 
 | ||||
|             expected = { | ||||
|                 'requested': override, | ||||
|                 'override__initial': 0, | ||||
|                 'override_after': override, | ||||
|                 'override_restored': 0, | ||||
|                 # The override should not affect the config or settings. | ||||
|                 'settings__initial': settings, | ||||
|                 'settings_after': settings, | ||||
|                 'settings_restored': settings, | ||||
|                 # These are the most likely values to be wrong. | ||||
|                 'allowed__initial': not enabled, | ||||
|                 'allowed_after': not ((override > 0) if override else enabled), | ||||
|                 'allowed_restored': not enabled, | ||||
|             } | ||||
| 
 | ||||
|             r, w = os.pipe() | ||||
|             script = textwrap.dedent(f''' | ||||
|                 from test.test_capi.check_config import run_singlephase_check | ||||
|                 run_singlephase_check({override}, {w}) | ||||
|                 ''') | ||||
|             with os.fdopen(r) as stdout: | ||||
|                 ret = support.run_in_subinterp_with_config(script, **kwargs) | ||||
|                 self.assertEqual(ret, 0) | ||||
|                 out = stdout.read() | ||||
|             results = json.loads(out) | ||||
| 
 | ||||
|             self.assertEqual(results, expected) | ||||
| 
 | ||||
|         self.maxDiff = None | ||||
| 
 | ||||
|         # setting: check disabled | ||||
|         with self.subTest('config: check disabled; override: disabled'): | ||||
|             check(False, -1) | ||||
|         with self.subTest('config: check disabled; override: use config'): | ||||
|             check(False, 0) | ||||
|         with self.subTest('config: check disabled; override: enabled'): | ||||
|             check(False, 1) | ||||
| 
 | ||||
|         # setting: check enabled | ||||
|         with self.subTest('config: check enabled; override: disabled'): | ||||
|             check(True, -1) | ||||
|         with self.subTest('config: check enabled; override: use config'): | ||||
|             check(True, 0) | ||||
|         with self.subTest('config: check enabled; override: enabled'): | ||||
|             check(True, 1) | ||||
| 
 | ||||
|     def test_mutate_exception(self): | ||||
|         """ | ||||
|         Exceptions saved in global module state get shared between | ||||
|  |  | |||
|  | @ -1656,13 +1656,15 @@ def test_init_use_frozen_modules(self): | |||
|                                        api=API_PYTHON, env=env) | ||||
| 
 | ||||
|     def test_init_main_interpreter_settings(self): | ||||
|         EXTENSIONS = 1<<8 | ||||
|         THREADS = 1<<10 | ||||
|         DAEMON_THREADS = 1<<11 | ||||
|         FORK = 1<<15 | ||||
|         EXEC = 1<<16 | ||||
|         expected = { | ||||
|             # All optional features should be enabled. | ||||
|             'feature_flags': FORK | EXEC | THREADS | DAEMON_THREADS, | ||||
|             'feature_flags': | ||||
|                 FORK | EXEC | THREADS | DAEMON_THREADS, | ||||
|         } | ||||
|         out, err = self.run_embedded_interpreter( | ||||
|             'test_init_main_interpreter_settings', | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ | |||
| from test.support import os_helper | ||||
| from test.support import ( | ||||
|     STDLIB_DIR, swap_attr, swap_item, cpython_only, is_emscripten, | ||||
|     is_wasi) | ||||
|     is_wasi, run_in_subinterp_with_config) | ||||
| from test.support.import_helper import ( | ||||
|     forget, make_legacy_pyc, unlink, unload, DirsOnSysPath, CleanImport) | ||||
| from test.support.os_helper import ( | ||||
|  | @ -30,6 +30,14 @@ | |||
| from test.support import threading_helper | ||||
| from test.test_importlib.util import uncache | ||||
| from types import ModuleType | ||||
| try: | ||||
|     import _testsinglephase | ||||
| except ImportError: | ||||
|     _testsinglephase = None | ||||
| try: | ||||
|     import _testmultiphase | ||||
| except ImportError: | ||||
|     _testmultiphase = None | ||||
| 
 | ||||
| 
 | ||||
| skip_if_dont_write_bytecode = unittest.skipIf( | ||||
|  | @ -1392,6 +1400,216 @@ def test_unwritable_module(self): | |||
|             unwritable.x = 42 | ||||
| 
 | ||||
| 
 | ||||
| class SubinterpImportTests(unittest.TestCase): | ||||
| 
 | ||||
|     RUN_KWARGS = dict( | ||||
|         allow_fork=False, | ||||
|         allow_exec=False, | ||||
|         allow_threads=True, | ||||
|         allow_daemon_threads=False, | ||||
|     ) | ||||
| 
 | ||||
|     @unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()") | ||||
|     def pipe(self): | ||||
|         r, w = os.pipe() | ||||
|         self.addCleanup(os.close, r) | ||||
|         self.addCleanup(os.close, w) | ||||
|         if hasattr(os, 'set_blocking'): | ||||
|             os.set_blocking(r, False) | ||||
|         return (r, w) | ||||
| 
 | ||||
|     def import_script(self, name, fd, check_override=None): | ||||
|         override_text = '' | ||||
|         if check_override is not None: | ||||
|             override_text = f''' | ||||
|             import _imp | ||||
|             _imp._override_multi_interp_extensions_check({check_override}) | ||||
|             ''' | ||||
|         return textwrap.dedent(f''' | ||||
|             import os, sys | ||||
|             {override_text} | ||||
|             try: | ||||
|                 import {name} | ||||
|             except ImportError as exc: | ||||
|                 text = 'ImportError: ' + str(exc) | ||||
|             else: | ||||
|                 text = 'okay' | ||||
|             os.write({fd}, text.encode('utf-8')) | ||||
|             ''') | ||||
| 
 | ||||
|     def run_shared(self, name, *, | ||||
|                    check_singlephase_setting=False, | ||||
|                    check_singlephase_override=None, | ||||
|                    ): | ||||
|         """ | ||||
|         Try importing the named module in a subinterpreter. | ||||
| 
 | ||||
|         The subinterpreter will be in the current process. | ||||
|         The module will have already been imported in the main interpreter. | ||||
|         Thus, for extension/builtin modules, the module definition will | ||||
|         have been loaded already and cached globally. | ||||
| 
 | ||||
|         "check_singlephase_setting" determines whether or not | ||||
|         the interpreter will be configured to check for modules | ||||
|         that are not compatible with use in multiple interpreters. | ||||
| 
 | ||||
|         This should always return "okay" for all modules if the | ||||
|         setting is False (with no override). | ||||
|         """ | ||||
|         __import__(name) | ||||
| 
 | ||||
|         kwargs = dict( | ||||
|             **self.RUN_KWARGS, | ||||
|             check_multi_interp_extensions=check_singlephase_setting, | ||||
|         ) | ||||
| 
 | ||||
|         r, w = self.pipe() | ||||
|         script = self.import_script(name, w, check_singlephase_override) | ||||
| 
 | ||||
|         ret = run_in_subinterp_with_config(script, **kwargs) | ||||
|         self.assertEqual(ret, 0) | ||||
|         return os.read(r, 100) | ||||
| 
 | ||||
|     def check_compatible_shared(self, name, *, strict=False): | ||||
|         # Verify that the named module may be imported in a subinterpreter. | ||||
|         # (See run_shared() for more info.) | ||||
|         out = self.run_shared(name, check_singlephase_setting=strict) | ||||
|         self.assertEqual(out, b'okay') | ||||
| 
 | ||||
|     def check_incompatible_shared(self, name): | ||||
|         # Differences from check_compatible_shared(): | ||||
|         #  * verify that import fails | ||||
|         #  * "strict" is always True | ||||
|         out = self.run_shared(name, check_singlephase_setting=True) | ||||
|         self.assertEqual( | ||||
|             out.decode('utf-8'), | ||||
|             f'ImportError: module {name} does not support loading in subinterpreters', | ||||
|         ) | ||||
| 
 | ||||
|     def check_compatible_isolated(self, name, *, strict=False): | ||||
|         # Differences from check_compatible_shared(): | ||||
|         #  * subinterpreter in a new process | ||||
|         #  * module has never been imported before in that process | ||||
|         #  * this tests importing the module for the first time | ||||
|         _, out, err = script_helper.assert_python_ok('-c', textwrap.dedent(f''' | ||||
|             import _testcapi, sys | ||||
|             assert ( | ||||
|                 {name!r} in sys.builtin_module_names or | ||||
|                 {name!r} not in sys.modules | ||||
|             ), repr({name!r}) | ||||
|             ret = _testcapi.run_in_subinterp_with_config( | ||||
|                 {self.import_script(name, "sys.stdout.fileno()")!r}, | ||||
|                 **{self.RUN_KWARGS}, | ||||
|                 check_multi_interp_extensions={strict}, | ||||
|             ) | ||||
|             assert ret == 0, ret | ||||
|             ''')) | ||||
|         self.assertEqual(err, b'') | ||||
|         self.assertEqual(out, b'okay') | ||||
| 
 | ||||
|     def check_incompatible_isolated(self, name): | ||||
|         # Differences from check_compatible_isolated(): | ||||
|         #  * verify that import fails | ||||
|         #  * "strict" is always True | ||||
|         _, out, err = script_helper.assert_python_ok('-c', textwrap.dedent(f''' | ||||
|             import _testcapi, sys | ||||
|             assert {name!r} not in sys.modules, {name!r} | ||||
|             ret = _testcapi.run_in_subinterp_with_config( | ||||
|                 {self.import_script(name, "sys.stdout.fileno()")!r}, | ||||
|                 **{self.RUN_KWARGS}, | ||||
|                 check_multi_interp_extensions=True, | ||||
|             ) | ||||
|             assert ret == 0, ret | ||||
|             ''')) | ||||
|         self.assertEqual(err, b'') | ||||
|         self.assertEqual( | ||||
|             out.decode('utf-8'), | ||||
|             f'ImportError: module {name} does not support loading in subinterpreters', | ||||
|         ) | ||||
| 
 | ||||
|     def test_builtin_compat(self): | ||||
|         module = 'sys' | ||||
|         with self.subTest(f'{module}: not strict'): | ||||
|             self.check_compatible_shared(module, strict=False) | ||||
|         with self.subTest(f'{module}: strict, shared'): | ||||
|             self.check_compatible_shared(module, strict=True) | ||||
| 
 | ||||
|     @cpython_only | ||||
|     def test_frozen_compat(self): | ||||
|         module = '_frozen_importlib' | ||||
|         if __import__(module).__spec__.origin != 'frozen': | ||||
|             raise unittest.SkipTest(f'{module} is unexpectedly not frozen') | ||||
|         with self.subTest(f'{module}: not strict'): | ||||
|             self.check_compatible_shared(module, strict=False) | ||||
|         with self.subTest(f'{module}: strict, shared'): | ||||
|             self.check_compatible_shared(module, strict=True) | ||||
| 
 | ||||
|     @unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module") | ||||
|     def test_single_init_extension_compat(self): | ||||
|         module = '_testsinglephase' | ||||
|         with self.subTest(f'{module}: not strict'): | ||||
|             self.check_compatible_shared(module, strict=False) | ||||
|         with self.subTest(f'{module}: strict, shared'): | ||||
|             self.check_incompatible_shared(module) | ||||
|         with self.subTest(f'{module}: strict, isolated'): | ||||
|             self.check_incompatible_isolated(module) | ||||
| 
 | ||||
|     @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") | ||||
|     def test_multi_init_extension_compat(self): | ||||
|         module = '_testmultiphase' | ||||
|         with self.subTest(f'{module}: not strict'): | ||||
|             self.check_compatible_shared(module, strict=False) | ||||
|         with self.subTest(f'{module}: strict, shared'): | ||||
|             self.check_compatible_shared(module, strict=True) | ||||
|         with self.subTest(f'{module}: strict, isolated'): | ||||
|             self.check_compatible_isolated(module, strict=True) | ||||
| 
 | ||||
|     def test_python_compat(self): | ||||
|         module = 'threading' | ||||
|         if __import__(module).__spec__.origin == 'frozen': | ||||
|             raise unittest.SkipTest(f'{module} is unexpectedly frozen') | ||||
|         with self.subTest(f'{module}: not strict'): | ||||
|             self.check_compatible_shared(module, strict=False) | ||||
|         with self.subTest(f'{module}: strict, shared'): | ||||
|             self.check_compatible_shared(module, strict=True) | ||||
|         with self.subTest(f'{module}: strict, isolated'): | ||||
|             self.check_compatible_isolated(module, strict=True) | ||||
| 
 | ||||
|     @unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module") | ||||
|     def test_singlephase_check_with_setting_and_override(self): | ||||
|         module = '_testsinglephase' | ||||
| 
 | ||||
|         def check_compatible(setting, override): | ||||
|             out = self.run_shared( | ||||
|                 module, | ||||
|                 check_singlephase_setting=setting, | ||||
|                 check_singlephase_override=override, | ||||
|             ) | ||||
|             self.assertEqual(out, b'okay') | ||||
| 
 | ||||
|         def check_incompatible(setting, override): | ||||
|             out = self.run_shared( | ||||
|                 module, | ||||
|                 check_singlephase_setting=setting, | ||||
|                 check_singlephase_override=override, | ||||
|             ) | ||||
|             self.assertNotEqual(out, b'okay') | ||||
| 
 | ||||
|         with self.subTest('config: check enabled; override: enabled'): | ||||
|             check_incompatible(True, 1) | ||||
|         with self.subTest('config: check enabled; override: use config'): | ||||
|             check_incompatible(True, 0) | ||||
|         with self.subTest('config: check enabled; override: disabled'): | ||||
|             check_compatible(True, -1) | ||||
| 
 | ||||
|         with self.subTest('config: check disabled; override: enabled'): | ||||
|             check_incompatible(False, 1) | ||||
|         with self.subTest('config: check disabled; override: use config'): | ||||
|             check_compatible(False, 0) | ||||
|         with self.subTest('config: check disabled; override: disabled'): | ||||
|             check_compatible(False, -1) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     # Test needs to be a package, so we can do relative imports. | ||||
|     unittest.main() | ||||
|  |  | |||
|  | @ -1347,6 +1347,7 @@ def func(): | |||
|                 allow_exec=True, | ||||
|                 allow_threads={allowed}, | ||||
|                 allow_daemon_threads={daemon_allowed}, | ||||
|                 check_multi_interp_extensions=False, | ||||
|             ) | ||||
|             """) | ||||
|         with test.support.SuppressCrashReport(): | ||||
|  |  | |||
|  | @ -0,0 +1,5 @@ | |||
| When an interpreter is configured to check (and only then), importing an | ||||
| extension module will now fail when the extension does not support multiple | ||||
| interpreters (i.e. doesn't implement PEP 489 multi-phase init). This does | ||||
| not apply to the main interpreter, nor to subinterpreters created with | ||||
| ``Py_NewInterpreter()``. | ||||
|  | @ -1618,6 +1618,7 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs) | |||
|     int allow_exec = -1; | ||||
|     int allow_threads = -1; | ||||
|     int allow_daemon_threads = -1; | ||||
|     int check_multi_interp_extensions = -1; | ||||
|     int r; | ||||
|     PyThreadState *substate, *mainstate; | ||||
|     /* only initialise 'cflags.cf_flags' to test backwards compatibility */ | ||||
|  | @ -1628,11 +1629,13 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs) | |||
|                              "allow_exec", | ||||
|                              "allow_threads", | ||||
|                              "allow_daemon_threads", | ||||
|                              "check_multi_interp_extensions", | ||||
|                              NULL}; | ||||
|     if (!PyArg_ParseTupleAndKeywords(args, kwargs, | ||||
|                     "s$pppp:run_in_subinterp_with_config", kwlist, | ||||
|                     "s$ppppp:run_in_subinterp_with_config", kwlist, | ||||
|                     &code, &allow_fork, &allow_exec, | ||||
|                     &allow_threads, &allow_daemon_threads)) { | ||||
|                     &allow_threads, &allow_daemon_threads, | ||||
|                     &check_multi_interp_extensions)) { | ||||
|         return NULL; | ||||
|     } | ||||
|     if (allow_fork < 0) { | ||||
|  | @ -1651,6 +1654,10 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs) | |||
|         PyErr_SetString(PyExc_ValueError, "missing allow_daemon_threads"); | ||||
|         return NULL; | ||||
|     } | ||||
|     if (check_multi_interp_extensions < 0) { | ||||
|         PyErr_SetString(PyExc_ValueError, "missing check_multi_interp_extensions"); | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     mainstate = PyThreadState_Get(); | ||||
| 
 | ||||
|  | @ -1661,6 +1668,7 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs) | |||
|         .allow_exec = allow_exec, | ||||
|         .allow_threads = allow_threads, | ||||
|         .allow_daemon_threads = allow_daemon_threads, | ||||
|         .check_multi_interp_extensions = check_multi_interp_extensions, | ||||
|     }; | ||||
|     substate = _Py_NewInterpreterFromConfig(&config); | ||||
|     if (substate == NULL) { | ||||
|  |  | |||
							
								
								
									
										33
									
								
								Python/clinic/import.c.h
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										33
									
								
								Python/clinic/import.c.h
									
										
									
										generated
									
									
									
								
							|  | @ -442,6 +442,37 @@ exit: | |||
|     return return_value; | ||||
| } | ||||
| 
 | ||||
| PyDoc_STRVAR(_imp__override_multi_interp_extensions_check__doc__, | ||||
| "_override_multi_interp_extensions_check($module, override, /)\n" | ||||
| "--\n" | ||||
| "\n" | ||||
| "(internal-only) Override PyInterpreterConfig.check_multi_interp_extensions.\n" | ||||
| "\n" | ||||
| "(-1: \"never\", 1: \"always\", 0: no override)"); | ||||
| 
 | ||||
| #define _IMP__OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK_METHODDEF    \ | ||||
|     {"_override_multi_interp_extensions_check", (PyCFunction)_imp__override_multi_interp_extensions_check, METH_O, _imp__override_multi_interp_extensions_check__doc__}, | ||||
| 
 | ||||
| static PyObject * | ||||
| _imp__override_multi_interp_extensions_check_impl(PyObject *module, | ||||
|                                                   int override); | ||||
| 
 | ||||
| static PyObject * | ||||
| _imp__override_multi_interp_extensions_check(PyObject *module, PyObject *arg) | ||||
| { | ||||
|     PyObject *return_value = NULL; | ||||
|     int override; | ||||
| 
 | ||||
|     override = _PyLong_AsInt(arg); | ||||
|     if (override == -1 && PyErr_Occurred()) { | ||||
|         goto exit; | ||||
|     } | ||||
|     return_value = _imp__override_multi_interp_extensions_check_impl(module, override); | ||||
| 
 | ||||
| exit: | ||||
|     return return_value; | ||||
| } | ||||
| 
 | ||||
| #if defined(HAVE_DYNAMIC_LOADING) | ||||
| 
 | ||||
| PyDoc_STRVAR(_imp_create_dynamic__doc__, | ||||
|  | @ -617,4 +648,4 @@ exit: | |||
| #ifndef _IMP_EXEC_DYNAMIC_METHODDEF | ||||
|     #define _IMP_EXEC_DYNAMIC_METHODDEF | ||||
| #endif /* !defined(_IMP_EXEC_DYNAMIC_METHODDEF) */ | ||||
| /*[clinic end generated code: output=806352838c3f7008 input=a9049054013a1b77]*/ | ||||
| /*[clinic end generated code: output=b18d46e0036eff49 input=a9049054013a1b77]*/ | ||||
|  |  | |||
|  | @ -74,6 +74,8 @@ static struct _inittab *inittab_copy = NULL; | |||
|     (interp)->imports.modules_by_index | ||||
| #define IMPORTLIB(interp) \ | ||||
|     (interp)->imports.importlib | ||||
| #define OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp) \ | ||||
|     (interp)->imports.override_multi_interp_extensions_check | ||||
| #define OVERRIDE_FROZEN_MODULES(interp) \ | ||||
|     (interp)->imports.override_frozen_modules | ||||
| #ifdef HAVE_DLOPEN | ||||
|  | @ -816,6 +818,38 @@ _extensions_cache_clear_all(void) | |||
|     Py_CLEAR(EXTENSIONS); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| static bool | ||||
| check_multi_interp_extensions(PyInterpreterState *interp) | ||||
| { | ||||
|     int override = OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp); | ||||
|     if (override < 0) { | ||||
|         return false; | ||||
|     } | ||||
|     else if (override > 0) { | ||||
|         return true; | ||||
|     } | ||||
|     else if (_PyInterpreterState_HasFeature( | ||||
|                 interp, Py_RTFLAGS_MULTI_INTERP_EXTENSIONS)) { | ||||
|         return true; | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| int | ||||
| _PyImport_CheckSubinterpIncompatibleExtensionAllowed(const char *name) | ||||
| { | ||||
|     PyInterpreterState *interp = _PyInterpreterState_Get(); | ||||
|     if (check_multi_interp_extensions(interp)) { | ||||
|         assert(!_Py_IsMainInterpreter(interp)); | ||||
|         PyErr_Format(PyExc_ImportError, | ||||
|                      "module %s does not support loading in subinterpreters", | ||||
|                      name); | ||||
|         return -1; | ||||
|     } | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| static int | ||||
| fix_up_extension(PyObject *mod, PyObject *name, PyObject *filename) | ||||
| { | ||||
|  | @ -3297,6 +3331,34 @@ _imp__override_frozen_modules_for_tests_impl(PyObject *module, int override) | |||
|     Py_RETURN_NONE; | ||||
| } | ||||
| 
 | ||||
| /*[clinic input]
 | ||||
| _imp._override_multi_interp_extensions_check | ||||
| 
 | ||||
|     override: int | ||||
|     / | ||||
| 
 | ||||
| (internal-only) Override PyInterpreterConfig.check_multi_interp_extensions. | ||||
| 
 | ||||
| (-1: "never", 1: "always", 0: no override) | ||||
| [clinic start generated code]*/ | ||||
| 
 | ||||
| static PyObject * | ||||
| _imp__override_multi_interp_extensions_check_impl(PyObject *module, | ||||
|                                                   int override) | ||||
| /*[clinic end generated code: output=3ff043af52bbf280 input=e086a2ea181f92ae]*/ | ||||
| { | ||||
|     PyInterpreterState *interp = _PyInterpreterState_GET(); | ||||
|     if (_Py_IsMainInterpreter(interp)) { | ||||
|         PyErr_SetString(PyExc_RuntimeError, | ||||
|                         "_imp._override_multi_interp_extensions_check() " | ||||
|                         "cannot be used in the main interpreter"); | ||||
|         return NULL; | ||||
|     } | ||||
|     int oldvalue = OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp); | ||||
|     OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp) = override; | ||||
|     return PyLong_FromLong(oldvalue); | ||||
| } | ||||
| 
 | ||||
| #ifdef HAVE_DYNAMIC_LOADING | ||||
| 
 | ||||
| /*[clinic input]
 | ||||
|  | @ -3329,18 +3391,23 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file) | |||
| 
 | ||||
|     PyThreadState *tstate = _PyThreadState_GET(); | ||||
|     mod = import_find_extension(tstate, name, path); | ||||
|     if (mod != NULL || PyErr_Occurred()) { | ||||
|         Py_DECREF(name); | ||||
|         Py_DECREF(path); | ||||
|         return mod; | ||||
|     if (mod != NULL) { | ||||
|         const char *name_buf = PyUnicode_AsUTF8(name); | ||||
|         assert(name_buf != NULL); | ||||
|         if (_PyImport_CheckSubinterpIncompatibleExtensionAllowed(name_buf) < 0) { | ||||
|             Py_DECREF(mod); | ||||
|             mod = NULL; | ||||
|         } | ||||
|         goto finally; | ||||
|     } | ||||
|     else if (PyErr_Occurred()) { | ||||
|         goto finally; | ||||
|     } | ||||
| 
 | ||||
|     if (file != NULL) { | ||||
|         fp = _Py_fopen_obj(path, "r"); | ||||
|         if (fp == NULL) { | ||||
|             Py_DECREF(name); | ||||
|             Py_DECREF(path); | ||||
|             return NULL; | ||||
|             goto finally; | ||||
|         } | ||||
|     } | ||||
|     else | ||||
|  | @ -3348,10 +3415,12 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file) | |||
| 
 | ||||
|     mod = _PyImport_LoadDynamicModuleWithSpec(spec, fp); | ||||
| 
 | ||||
|     Py_DECREF(name); | ||||
|     Py_DECREF(path); | ||||
|     if (fp) | ||||
|         fclose(fp); | ||||
| 
 | ||||
| finally: | ||||
|     Py_DECREF(name); | ||||
|     Py_DECREF(path); | ||||
|     return mod; | ||||
| } | ||||
| 
 | ||||
|  | @ -3436,6 +3505,7 @@ static PyMethodDef imp_methods[] = { | |||
|     _IMP_IS_FROZEN_METHODDEF | ||||
|     _IMP__FROZEN_MODULE_NAMES_METHODDEF | ||||
|     _IMP__OVERRIDE_FROZEN_MODULES_FOR_TESTS_METHODDEF | ||||
|     _IMP__OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK_METHODDEF | ||||
|     _IMP_CREATE_DYNAMIC_METHODDEF | ||||
|     _IMP_EXEC_DYNAMIC_METHODDEF | ||||
|     _IMP_EXEC_BUILTIN_METHODDEF | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ | |||
| 
 | ||||
| #include "Python.h" | ||||
| #include "pycore_call.h" | ||||
| #include "pycore_import.h" | ||||
| #include "pycore_pystate.h" | ||||
| #include "pycore_runtime.h" | ||||
| 
 | ||||
|  | @ -203,6 +204,10 @@ _PyImport_LoadDynamicModuleWithSpec(PyObject *spec, FILE *fp) | |||
| 
 | ||||
|     /* Fall back to single-phase init mechanism */ | ||||
| 
 | ||||
|     if (_PyImport_CheckSubinterpIncompatibleExtensionAllowed(name_buf) < 0) { | ||||
|         goto error; | ||||
|     } | ||||
| 
 | ||||
|     if (hook_prefix == nonascii_prefix) { | ||||
|         /* don't allow legacy init for non-ASCII module names */ | ||||
|         PyErr_Format( | ||||
|  |  | |||
|  | @ -565,6 +565,10 @@ init_interp_settings(PyInterpreterState *interp, const _PyInterpreterConfig *con | |||
|     if (config->allow_daemon_threads) { | ||||
|         interp->feature_flags |= Py_RTFLAGS_DAEMON_THREADS; | ||||
|     } | ||||
| 
 | ||||
|     if (config->check_multi_interp_extensions) { | ||||
|         interp->feature_flags |= Py_RTFLAGS_MULTI_INTERP_EXTENSIONS; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Eric Snow
						Eric Snow