diff --git a/Doc/c-api/module.rst b/Doc/c-api/module.rst index 39293b0fa228df..8b967c285ac865 100644 --- a/Doc/c-api/module.rst +++ b/Doc/c-api/module.rst @@ -230,6 +230,9 @@ Feature slots When creating a module, Python checks the value of this slot using :c:func:`PyABIInfo_Check`. + This slot is required, except for modules created from + :c:struct:`PyModuleDef`. + .. versionadded:: 3.15 .. c:macro:: Py_mod_multiple_interpreters @@ -620,9 +623,9 @@ rather than from an extension's :ref:`export hook `. and the :py:class:`~importlib.machinery.ModuleSpec` *spec*. The *slots* argument must point to an array of :c:type:`PyModuleDef_Slot` - structures, terminated by an entry slot with slot ID of 0 + structures, terminated by an entry with slot ID of 0 (typically written as ``{0}`` or ``{0, NULL}`` in C). - The *slots* argument may not be ``NULL``. + The array must include a :c:data:`Py_mod_abi` entry. The *spec* argument may be any ``ModuleSpec``-like object, as described in :c:macro:`Py_mod_create` documentation. diff --git a/Doc/extending/first-extension-module.rst b/Doc/extending/first-extension-module.rst index f1ba0a3ceb7dba..cd755a98f7f5f4 100644 --- a/Doc/extending/first-extension-module.rst +++ b/Doc/extending/first-extension-module.rst @@ -265,12 +265,19 @@ Define this array just before your export hook: .. code-block:: c + PyABIInfo_VAR(abi_info); + static PyModuleDef_Slot spam_slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_name, "spam"}, {Py_mod_doc, "A wonderful module with an example function"}, {0, NULL} }; +The ``PyABIInfo_VAR(abi_info);`` macro and the :c:data:`Py_mod_abi` slot +are a bit of boilerplate that helps prevent extensions compiled for +a different version of Python from crashing the interpreter. + For both :c:data:`Py_mod_name` and :c:data:`Py_mod_doc`, the values are C strings -- that is, NUL-terminated, UTF-8 encoded byte arrays. diff --git a/Doc/includes/capi-extension/spammodule-01.c b/Doc/includes/capi-extension/spammodule-01.c index ac96f17f04712c..0bc34ef57445cb 100644 --- a/Doc/includes/capi-extension/spammodule-01.c +++ b/Doc/includes/capi-extension/spammodule-01.c @@ -35,7 +35,10 @@ static PyMethodDef spam_methods[] = { /// Module slot table +PyABIInfo_VAR(abi_info); + static PyModuleDef_Slot spam_slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_name, "spam"}, {Py_mod_doc, "A wonderful module with an example function"}, {Py_mod_methods, spam_methods}, diff --git a/Include/cpython/longintrepr.h b/Include/cpython/longintrepr.h index 804c1e9427e063..998ebe6891577e 100644 --- a/Include/cpython/longintrepr.h +++ b/Include/cpython/longintrepr.h @@ -133,6 +133,11 @@ _PyLong_CompactValue(const PyLongObject *op) assert(PyType_HasFeature(op->ob_base.ob_type, Py_TPFLAGS_LONG_SUBCLASS)); assert(PyUnstable_Long_IsCompact(op)); sign = 1 - (op->long_value.lv_tag & _PyLong_SIGN_MASK); + if (sign == 0) { + // gh-147988: Make sure that the digit is zero. + // It helps detecting the usage of uninitialized digits. + assert(op->long_value.ob_digit[0] == 0); + } return sign * (Py_ssize_t)op->long_value.ob_digit[0]; } diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index c86cd58e295e53..9fd3be74404907 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -211,16 +211,16 @@ extern void _PyEval_DeactivateOpCache(void); /* --- _Py_EnterRecursiveCall() ----------------------------------------- */ -static inline int _Py_ReachedRecursionLimit(PyThreadState *tstate) { +static inline int _Py_MakeRecCheck(PyThreadState *tstate) { uintptr_t here_addr = _Py_get_machine_stack_pointer(); _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate; - // Possible overflow if stack pointer is beyond the soft limit. - // _Py_CheckRecursiveCall will check for corner cases and - // report an error if there is an overflow. + // Overflow if stack pointer is between soft limit and the base of the hardware stack. + // If it is below the hardware stack base, assume that we have the wrong stack limits, and do nothing. + // We could have the wrong stack limits because of limited platform support, or user-space threads. #if _Py_STACK_GROWS_DOWN - return here_addr < _tstate->c_stack_soft_limit; + return here_addr < _tstate->c_stack_soft_limit && here_addr >= _tstate->c_stack_soft_limit - 2 * _PyOS_STACK_MARGIN_BYTES; #else - return here_addr > _tstate->c_stack_soft_limit; + return here_addr > _tstate->c_stack_soft_limit && here_addr <= _tstate->c_stack_soft_limit + 2 * _PyOS_STACK_MARGIN_BYTES; #endif } @@ -235,7 +235,7 @@ PyAPI_FUNC(int) _Py_CheckRecursiveCallPy( static inline int _Py_EnterRecursiveCallTstate(PyThreadState *tstate, const char *where) { - return (_Py_ReachedRecursionLimit(tstate) && _Py_CheckRecursiveCall(tstate, where)); + return (_Py_MakeRecCheck(tstate) && _Py_CheckRecursiveCall(tstate, where)); } static inline int _Py_EnterRecursiveCall(const char *where) { @@ -249,6 +249,8 @@ static inline void _Py_LeaveRecursiveCallTstate(PyThreadState *tstate) { PyAPI_FUNC(void) _Py_InitializeRecursionLimits(PyThreadState *tstate); +PyAPI_FUNC(int) _Py_ReachedRecursionLimit(PyThreadState *tstate); + // Export for test_peg_generator PyAPI_FUNC(int) _Py_ReachedRecursionLimitWithMargin( PyThreadState *tstate, diff --git a/Include/internal/pycore_pystate.h b/Include/internal/pycore_pystate.h index 054360d69e6fae..189a8dde9f09ed 100644 --- a/Include/internal/pycore_pystate.h +++ b/Include/internal/pycore_pystate.h @@ -306,23 +306,23 @@ _Py_AssertHoldsTstateFunc(const char *func) #define _Py_AssertHoldsTstate() #endif +#if !_Py__has_builtin(__builtin_frame_address) && !defined(__GNUC__) && !defined(_MSC_VER) +static uintptr_t return_pointer_as_int(char* p) { + return (uintptr_t)p; +} +#endif static inline uintptr_t _Py_get_machine_stack_pointer(void) { - uintptr_t result; -#if !defined(_MSC_VER) && defined(_M_ARM64) - result = __getReg(31); -#elif defined(_MSC_VER) && defined(_M_X64) - result = (uintptr_t)_AddressOfReturnAddress(); -#elif defined(__aarch64__) - __asm__ ("mov %0, sp" : "=r" (result)); -#elif defined(__x86_64__) - __asm__("{movq %%rsp, %0" : "=r" (result)); +#if _Py__has_builtin(__builtin_frame_address) || defined(__GNUC__) + return (uintptr_t)__builtin_frame_address(0); +#elif defined(_MSC_VER) + return (uintptr_t)_AddressOfReturnAddress(); #else char here; - result = (uintptr_t)&here; + /* Avoid compiler warning about returning stack address */ + return return_pointer_as_int(&here); #endif - return result; } static inline intptr_t diff --git a/Include/internal/pycore_pythonrun.h b/Include/internal/pycore_pythonrun.h index 66dd7cd843b04f..2a544edc431e6b 100644 --- a/Include/internal/pycore_pythonrun.h +++ b/Include/internal/pycore_pythonrun.h @@ -46,8 +46,7 @@ extern PyObject * _Py_CompileStringObjectWithModule( * stack consumption of PyEval_EvalDefault */ #if (defined(Py_DEBUG) \ || defined(_Py_ADDRESS_SANITIZER) \ - || defined(_Py_THREAD_SANITIZER)) \ - || defined(_Py_UNDEFINED_BEHAVIOR_SANITIZER) + || defined(_Py_THREAD_SANITIZER)) # define _PyOS_LOG2_STACK_MARGIN 12 #else # define _PyOS_LOG2_STACK_MARGIN 11 diff --git a/Include/pyport.h b/Include/pyport.h index ee90711c202482..f7bb5d513b9ae6 100644 --- a/Include/pyport.h +++ b/Include/pyport.h @@ -598,11 +598,6 @@ extern "C" { # define _Py_NO_SANITIZE_THREAD __attribute__((no_sanitize_thread)) # endif # endif -# if __has_feature(undefined_behavior_sanitizer) -# if !defined(_Py_UNDEFINED_BEHAVIOR_SANITIZER) -# define _Py_UNDEFINED_BEHAVIOR_SANITIZER -# endif -# endif #elif defined(__GNUC__) # if defined(__SANITIZE_ADDRESS__) # define _Py_ADDRESS_SANITIZER diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 3da662b0c4d50a..8ff061e074074f 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -71,7 +71,8 @@ "BrokenIter", "in_systemd_nspawn_sync_suppressed", "run_no_yield_async_fn", "run_yielding_async_fn", "async_yield", - "reset_code", "on_github_actions" + "reset_code", "on_github_actions", + "requires_root_user", "requires_non_root_user", ] @@ -3317,3 +3318,8 @@ def control_characters_c0() -> list[str]: C0 control characters defined as the byte range 0x00-0x1F, and 0x7F. """ return [chr(c) for c in range(0x00, 0x20)] + ["\x7F"] + + +_ROOT_IN_POSIX = hasattr(os, 'geteuid') and os.geteuid() == 0 +requires_root_user = unittest.skipUnless(_ROOT_IN_POSIX, "test needs root privilege") +requires_non_root_user = unittest.skipIf(_ROOT_IN_POSIX, "test needs non-root account") diff --git a/Lib/test/test_capi/test_long.py b/Lib/test/test_capi/test_long.py index fc0454b71cb780..d1467caf6880a6 100644 --- a/Lib/test/test_capi/test_long.py +++ b/Lib/test/test_capi/test_long.py @@ -803,6 +803,16 @@ def to_digits(num): self.assertEqual(pylongwriter_create(negative, digits), num, (negative, digits)) + @unittest.skipUnless(support.Py_DEBUG, "need a debug build (Py_DEBUG)") + def test_longwriter_finish(self): + # Test PyLongWriter_Create(0, 3, &digits) with PyLongWriter_Finish() + # where the last digit is left uninitialized + pylongwriter_finish_bug = _testcapi.pylongwriter_finish_bug + with self.assertRaises(SystemError) as cm: + pylongwriter_finish_bug() + self.assertEqual(str(cm.exception), + 'PyLongWriter_Finish: digit 2 is uninitialized') + def test_bug_143050(self): with support.adjust_int_max_str_digits(0): # Bug coming from using _pylong.int_from_string(), that diff --git a/Lib/test/test_capi/test_module.py b/Lib/test/test_capi/test_module.py index 053e6709cda42e..c32ca1098edc56 100644 --- a/Lib/test/test_capi/test_module.py +++ b/Lib/test/test_capi/test_module.py @@ -25,9 +25,13 @@ def def_and_token(mod): ) class TestModFromSlotsAndSpec(unittest.TestCase): - @requires_gil_enabled("empty slots re-enable GIL") def test_empty(self): - mod = _testcapi.module_from_slots_empty(FakeSpec()) + with self.assertRaises(SystemError): + _testcapi.module_from_slots_empty(FakeSpec()) + + @requires_gil_enabled("minimal slots re-enable GIL") + def test_minimal(self): + mod = _testcapi.module_from_slots_minimal(FakeSpec()) self.assertIsInstance(mod, types.ModuleType) self.assertEqual(def_and_token(mod), (0, 0)) self.assertEqual(mod.__name__, 'testmod') @@ -159,6 +163,16 @@ def test_null_def_slot(self): self.assertIn(name, str(cm.exception)) self.assertIn("NULL", str(cm.exception)) + def test_bad_abiinfo(self): + """Slots that incompatible ABI is rejected""" + with self.assertRaises(ImportError) as cm: + _testcapi.module_from_bad_abiinfo(FakeSpec()) + + def test_multiple_abiinfo(self): + """Slots that Py_mod_abiinfo can be repeated""" + mod = _testcapi.module_from_multiple_abiinfo(FakeSpec()) + self.assertEqual(mod.__name__, 'testmod') + def test_def_multiple_exec(self): """PyModule_Exec runs all exec slots of PyModuleDef-defined module""" mod = _testcapi.module_from_def_multiple_exec(FakeSpec()) diff --git a/Lib/test/test_cext/extension.c b/Lib/test/test_cext/extension.c index 7555b78f18c2e6..a880cb82811f78 100644 --- a/Lib/test/test_cext/extension.c +++ b/Lib/test/test_cext/extension.c @@ -119,8 +119,10 @@ _Py_COMP_DIAG_PUSH #endif PyDoc_STRVAR(_testcext_doc, "C test extension."); +PyABIInfo_VAR(abi_info); static PyModuleDef_Slot _testcext_slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_name, STR(MODULE_NAME)}, {Py_mod_doc, (void*)(char*)_testcext_doc}, {Py_mod_exec, (void*)_testcext_exec}, diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 437ab7031356b1..c905c0da0a1232 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -3499,12 +3499,20 @@ class Sub(tp): pass self.assertEqual(_testcapi.pytype_getmodulebytoken(Sub, token), module) - @requires_gil_enabled("empty slots re-enable GIL") def test_from_modexport_empty_slots(self): + # Module to test that Py_mod_abi is mandatory for PyModExport + modname = '_test_from_modexport_empty_slots' + filename = _testmultiphase.__file__ + with self.assertRaises(SystemError): + import_extension_from_file( + modname, filename, put_in_sys_modules=False) + + @requires_gil_enabled("this module re-enables GIL") + def test_from_modexport_minimal_slots(self): # Module to test that: - # - no slots are mandatory for PyModExport + # - no slots except Py_mod_abi is mandatory for PyModExport # - the slots array is used as the default token - modname = '_test_from_modexport_empty_slots' + modname = '_test_from_modexport_minimal_slots' filename = _testmultiphase.__file__ module = import_extension_from_file( modname, filename, put_in_sys_modules=False) @@ -3516,7 +3524,7 @@ def test_from_modexport_empty_slots(self): smoke_mod = import_extension_from_file( '_test_from_modexport_smoke', filename, put_in_sys_modules=False) self.assertEqual(_testcapi.pymodule_get_token(module), - smoke_mod.get_modexport_empty_slots()) + smoke_mod.get_modexport_minimal_slots()) @cpython_only class TestMagicNumber(unittest.TestCase): diff --git a/Lib/test/test_itertools.py b/Lib/test/test_itertools.py index dc64288085fa74..cf579d4da4e0df 100644 --- a/Lib/test/test_itertools.py +++ b/Lib/test/test_itertools.py @@ -754,6 +754,38 @@ def keys(): next(g) next(g) # must pass with address sanitizer + def test_grouper_reentrant_eq_does_not_crash(self): + # regression test for gh-146613 + grouper_iter = None + + class Key: + __hash__ = None + + def __init__(self, do_advance): + self.do_advance = do_advance + + def __eq__(self, other): + nonlocal grouper_iter + if self.do_advance: + self.do_advance = False + if grouper_iter is not None: + try: + next(grouper_iter) + except StopIteration: + pass + return NotImplemented + return True + + def keyfunc(element): + if element == 0: + return Key(do_advance=True) + return Key(do_advance=False) + + g = itertools.groupby(range(4), keyfunc) + key, grouper_iter = next(g) + items = list(grouper_iter) + self.assertEqual(len(items), 1) + def test_filter(self): self.assertEqual(list(filter(isEven, range(6))), [0,2,4]) self.assertEqual(list(filter(None, [0,1,0,2,0])), [1,2]) diff --git a/Lib/test/test_mailbox.py b/Lib/test/test_mailbox.py index 7421076ddd4c3a..019c699bff55c4 100644 --- a/Lib/test/test_mailbox.py +++ b/Lib/test/test_mailbox.py @@ -11,6 +11,7 @@ from test.support import import_helper, warnings_helper from test.support import os_helper from test.support import refleak_helper +from test.support import requires_root_user from test.support import socket_helper import unittest import textwrap @@ -1086,6 +1087,7 @@ def test_permissions_after_flush(self): self.assertEqual(os.stat(self._path).st_mode, mode) + @requires_root_user @unittest.skipUnless(hasattr(os, 'chown'), 'requires os.chown') def test_ownership_after_flush(self): # See issue gh-117467 @@ -1108,10 +1110,7 @@ def test_ownership_after_flush(self): else: self.skipTest("test needs more than one group") - try: - os.chown(self._path, other_uid, other_gid) - except OSError: - self.skipTest('test needs root privilege') + os.chown(self._path, other_uid, other_gid) # Change permissions as in test_permissions_after_flush. mode = st.st_mode | 0o666 os.chmod(self._path, mode) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 06f69caad12bc8..7e670e5a139d99 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -33,6 +33,8 @@ from test.support import os_helper from test.support import socket_helper from test.support import infinite_recursion +from test.support import requires_root_user +from test.support import requires_non_root_user from test.support import warnings_helper from platform import win32_is_iot from .utils import create_file @@ -67,10 +69,6 @@ from test.support.os_helper import FakePath -root_in_posix = False -if hasattr(os, 'geteuid'): - root_in_posix = (os.geteuid() == 0) - # Detect whether we're on a Linux system that uses the (now outdated # and unmaintained) linuxthreads threading library. There's an issue # when combining linuxthreads with a failed execv call: see @@ -2257,8 +2255,8 @@ def test_chown_gid(self): gid = os.stat(os_helper.TESTFN).st_gid self.assertEqual(gid, gid_2) - @unittest.skipUnless(root_in_posix and len(all_users) > 1, - "test needs root privilege and more than one user") + @requires_root_user + @unittest.skipUnless(len(all_users) > 1, "test needs more than one user") def test_chown_with_root(self): uid_1, uid_2 = all_users[:2] gid = os.stat(os_helper.TESTFN).st_gid @@ -2269,8 +2267,8 @@ def test_chown_with_root(self): uid = os.stat(os_helper.TESTFN).st_uid self.assertEqual(uid, uid_2) - @unittest.skipUnless(not root_in_posix and len(all_users) > 1, - "test needs non-root account and more than one user") + @requires_non_root_user + @unittest.skipUnless(len(all_users) > 1, "test needs and more than one user") def test_chown_without_permission(self): uid_1, uid_2 = all_users[:2] gid = os.stat(os_helper.TESTFN).st_gid diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index ef9ea0d11d06a6..d346a1a6160d6b 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -20,6 +20,7 @@ from test.support import is_emscripten, is_wasi, is_wasm32 from test.support import infinite_recursion from test.support import os_helper +from test.support import requires_root_user from test.support.os_helper import TESTFN, FS_NONASCII, FakePath try: import fcntl @@ -35,11 +36,6 @@ posix = None -root_in_posix = False -if hasattr(os, 'geteuid'): - root_in_posix = (os.geteuid() == 0) - - def patch_replace(old_test): def new_replace(self, target): raise OSError(errno.EXDEV, "Cross-device link", self, target) @@ -1554,7 +1550,7 @@ def raiser(*args, **kwargs): self.assertRaises(FileNotFoundError, source.copy, target) @unittest.skipIf(sys.platform == "win32" or sys.platform == "wasi", "directories are always readable on Windows and WASI") - @unittest.skipIf(root_in_posix, "test fails with root privilege") + @requires_root_user def test_copy_dir_no_read_permission(self): base = self.cls(self.base) source = base / 'dirE' @@ -2027,7 +2023,7 @@ def test_owner(self): self.assertEqual(expected_name, p.owner()) @unittest.skipUnless(pwd, "the pwd module is needed for this test") - @unittest.skipUnless(root_in_posix, "test needs root privilege") + @requires_root_user def test_owner_no_follow_symlinks(self): all_users = [u.pw_uid for u in pwd.getpwall()] if len(all_users) < 2: @@ -2062,7 +2058,7 @@ def test_group(self): self.assertEqual(expected_name, p.group()) @unittest.skipUnless(grp, "the grp module is needed for this test") - @unittest.skipUnless(root_in_posix, "test needs root privilege") + @requires_root_user def test_group_no_follow_symlinks(self): all_groups = [g.gr_gid for g in grp.getgrall()] if len(all_groups) < 2: diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index 0361d9f3da9069..cace780f79f515 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -707,7 +707,7 @@ def test_trigger_leak(self): def test_deeply_nested_content_model(self): # This should raise a RecursionError and not crash. # See https://github.com/python/cpython/issues/145986. - N = 800_000 + N = 500_000 data = ( b'>> exit()" - basic_output, basic_exit_code = self.run_repl(commands, env=env) - self.assertEqual(basic_exit_code, 0) - self.assertIn(expected_output_sequence, basic_output) + # gh-143394: The basic REPL needs the readline module to turn off + # ECHO terminal attribute. + if readline_module is not None: + basic_output, basic_exit_code = self.run_repl(commands, env=env) + self.assertEqual(basic_exit_code, 0) + self.assertIn(expected_output_sequence, basic_output) output, exit_code = self.run_repl(commands) self.assertEqual(exit_code, 0) diff --git a/Misc/NEWS.d/next/C_API/2026-03-31-13-33-41.gh-issue-146636.5do3wt.rst b/Misc/NEWS.d/next/C_API/2026-03-31-13-33-41.gh-issue-146636.5do3wt.rst new file mode 100644 index 00000000000000..8f8b832b8baee9 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-03-31-13-33-41.gh-issue-146636.5do3wt.rst @@ -0,0 +1,3 @@ +The :c:data:`Py_mod_abi` slot is now mandatory for modules created from a +slots array (using :c:func:`PyModule_FromSlotsAndSpec` or the +:c:func:`PyModExport_* ` export hook). diff --git a/Misc/NEWS.d/next/Library/2026-04-01-11-05-36.gh-issue-146613.GzjUFK.rst b/Misc/NEWS.d/next/Library/2026-04-01-11-05-36.gh-issue-146613.GzjUFK.rst new file mode 100644 index 00000000000000..94e198e7b28ad8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-01-11-05-36.gh-issue-146613.GzjUFK.rst @@ -0,0 +1,2 @@ +:mod:`itertools`: Fix a crash in :func:`itertools.groupby` when +the grouper iterator is concurrently mutated. diff --git a/Modules/_testcapi/long.c b/Modules/_testcapi/long.c index 6313abf5485fff..008a7d37726869 100644 --- a/Modules/_testcapi/long.c +++ b/Modules/_testcapi/long.c @@ -254,6 +254,25 @@ pylongwriter_create(PyObject *module, PyObject *args) } +static PyObject * +pylongwriter_finish_bug(PyObject *module, PyObject *Py_UNUSED(args)) +{ + void *writer_digits; + PyLongWriter *writer = PyLongWriter_Create(0, 3, &writer_digits); + if (writer == NULL) { + return NULL; + } + + assert(PyLong_GetNativeLayout()->digit_size == sizeof(digit)); + digit *digits = writer_digits; + digits[0] = 1; + digits[1] = 1; + // Oops, digits[2] is left uninitialized on purpose + // to test PyLongWriter_Finish() + return PyLongWriter_Finish(writer); +} + + static PyObject * get_pylong_layout(PyObject *module, PyObject *Py_UNUSED(args)) { @@ -271,6 +290,7 @@ static PyMethodDef test_methods[] = { {"pylong_aspid", pylong_aspid, METH_O}, {"pylong_export", pylong_export, METH_O}, {"pylongwriter_create", pylongwriter_create, METH_VARARGS}, + {"pylongwriter_finish_bug", pylongwriter_finish_bug, METH_NOARGS}, {"get_pylong_layout", get_pylong_layout, METH_NOARGS}, {"pylong_ispositive", pylong_ispositive, METH_O}, {"pylong_isnegative", pylong_isnegative, METH_O}, diff --git a/Modules/_testcapi/module.c b/Modules/_testcapi/module.c index 3411b21e942a19..52e1d6d94a3af7 100644 --- a/Modules/_testcapi/module.c +++ b/Modules/_testcapi/module.c @@ -8,6 +8,8 @@ * Lib/test/test_capi/test_module.py */ +PyABIInfo_VAR(abi_info); + static PyObject * module_from_slots_empty(PyObject *self, PyObject *spec) { @@ -17,6 +19,16 @@ module_from_slots_empty(PyObject *self, PyObject *spec) return PyModule_FromSlotsAndSpec(slots, spec); } +static PyObject * +module_from_slots_minimal(PyObject *self, PyObject *spec) +{ + PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, + {0}, + }; + return PyModule_FromSlotsAndSpec(slots, spec); +} + static PyObject * module_from_slots_null(PyObject *self, PyObject *spec) { @@ -27,6 +39,7 @@ static PyObject * module_from_slots_name(PyObject *self, PyObject *spec) { PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_name, "currently ignored..."}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, @@ -39,6 +52,7 @@ static PyObject * module_from_slots_doc(PyObject *self, PyObject *spec) { PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_doc, "the docstring"}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, @@ -51,6 +65,7 @@ static PyObject * module_from_slots_size(PyObject *self, PyObject *spec) { PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_state_size, (void*)123}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, @@ -78,6 +93,7 @@ static PyObject * module_from_slots_methods(PyObject *self, PyObject *spec) { PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_methods, a_methoddef_array}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, @@ -96,6 +112,7 @@ static PyObject * module_from_slots_gc(PyObject *self, PyObject *spec) { PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_state_traverse, noop_traverse}, {Py_mod_state_clear, noop_clear}, {Py_mod_state_free, noop_free}, @@ -128,6 +145,7 @@ static PyObject * module_from_slots_token(PyObject *self, PyObject *spec) { PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_token, (void*)&test_token}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, @@ -156,6 +174,7 @@ static PyObject * module_from_slots_exec(PyObject *self, PyObject *spec) { PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_exec, simple_exec}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, @@ -189,6 +208,7 @@ static PyObject * module_from_slots_create(PyObject *self, PyObject *spec) { PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_create, create_attr_from_spec}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, @@ -220,6 +240,7 @@ module_from_slots_repeat_slot(PyObject *self, PyObject *spec) return NULL; } PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {slot_id, "anything"}, {slot_id, "anything else"}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, @@ -238,6 +259,7 @@ module_from_slots_null_slot(PyObject *self, PyObject *spec) } PyModuleDef_Slot slots[] = { {slot_id, NULL}, + {Py_mod_abi, &abi_info}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, {0}, @@ -254,6 +276,7 @@ module_from_def_slot(PyObject *self, PyObject *spec) } PyModuleDef_Slot slots[] = { {slot_id, "anything"}, + {Py_mod_abi, &abi_info}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, {0}, @@ -285,6 +308,7 @@ static PyModuleDef parrot_def = { .m_slots = NULL /* set below */, }; static PyModuleDef_Slot parrot_slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_name, (void*)parrot_name}, {Py_mod_doc, (void*)parrot_doc}, {Py_mod_state_size, (void*)123}, @@ -314,6 +338,43 @@ module_from_def_slot_parrot(PyObject *self, PyObject *spec) return module; } +static PyObject * +module_from_bad_abiinfo(PyObject *self, PyObject *spec) +{ + PyABIInfo bad_abi_info = { + 1, 0, + .abi_version=0x02080000, + }; + PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, + {Py_mod_abi, &bad_abi_info}, + {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, + {0}, + }; + return PyModule_FromSlotsAndSpec(slots, spec); +} + +static PyObject * +module_from_multiple_abiinfo(PyObject *self, PyObject *spec) +{ + PyABIInfo extra_abi_info = { + 1, 0, + .flags=PyABIInfo_STABLE | PyABIInfo_FREETHREADING_AGNOSTIC, + .abi_version=0x03040000, + }; + PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, + {Py_mod_abi, &abi_info}, + {Py_mod_abi, &extra_abi_info}, + {Py_mod_abi, &extra_abi_info}, + {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, + {0}, + }; + return PyModule_FromSlotsAndSpec(slots, spec); +} + static int another_exec(PyObject *module) { @@ -344,6 +405,7 @@ static PyObject * module_from_def_multiple_exec(PyObject *self, PyObject *spec) { static PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_exec, simple_exec}, {Py_mod_exec, another_exec}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, @@ -399,6 +461,7 @@ pymodule_get_state_size(PyObject *self, PyObject *module) static PyMethodDef test_methods[] = { {"module_from_slots_empty", module_from_slots_empty, METH_O}, + {"module_from_slots_minimal", module_from_slots_minimal, METH_O}, {"module_from_slots_null", module_from_slots_null, METH_O}, {"module_from_slots_name", module_from_slots_name, METH_O}, {"module_from_slots_doc", module_from_slots_doc, METH_O}, @@ -413,6 +476,8 @@ static PyMethodDef test_methods[] = { {"module_from_def_multiple_exec", module_from_def_multiple_exec, METH_O}, {"module_from_def_slot", module_from_def_slot, METH_O}, {"module_from_def_slot_parrot", module_from_def_slot_parrot, METH_O}, + {"module_from_bad_abiinfo", module_from_bad_abiinfo, METH_O}, + {"module_from_multiple_abiinfo", module_from_multiple_abiinfo, METH_O}, {"pymodule_get_token", pymodule_get_token, METH_O}, {"pymodule_get_def", pymodule_get_def, METH_O}, {"pymodule_get_state_size", pymodule_get_state_size, METH_O}, diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index 4921dc90713daf..54f53c899f5e39 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -1034,10 +1034,13 @@ PyInit__test_no_multiple_interpreter_slot(void) /* PyModExport_* hooks */ +PyABIInfo_VAR(abi_info); + PyMODEXPORT_FUNC PyModExport__test_from_modexport(void) { static PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_name, "_test_from_modexport"}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, @@ -1050,6 +1053,7 @@ PyMODEXPORT_FUNC PyModExport__test_from_modexport_gil_used(void) { static PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_name, "_test_from_modexport_gil_used"}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_USED}, @@ -1100,6 +1104,7 @@ PyMODEXPORT_FUNC PyModExport__test_from_modexport_create_nonmodule(void) { static PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_name, "_test_from_modexport_create_nonmodule"}, {Py_mod_create, modexport_create_string}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, @@ -1113,6 +1118,7 @@ PyMODEXPORT_FUNC PyModExport__test_from_modexport_create_nonmodule_gil_used(void) { static PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_name, "_test_from_modexport_create_nonmodule"}, {Py_mod_create, modexport_create_string}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, @@ -1132,6 +1138,18 @@ PyModExport__test_from_modexport_empty_slots(void) return modexport_empty_slots; } + +static PyModuleDef_Slot modexport_minimal_slots[] = { + {Py_mod_abi, &abi_info}, + {0}, +}; + +PyMODEXPORT_FUNC +PyModExport__test_from_modexport_minimal_slots(void) +{ + return modexport_minimal_slots; +} + static int modexport_smoke_exec(PyObject *mod) { @@ -1172,13 +1190,13 @@ modexport_smoke_get_test_token(PyObject *mod, PyObject *arg) } static PyObject * -modexport_get_empty_slots(PyObject *mod, PyObject *arg) +modexport_get_minimal_slots(PyObject *mod, PyObject *arg) { /* Get the address of modexport_empty_slots. - * This method would be in the `_test_from_modexport_empty_slots` module, + * This method would be in the `_test_from_modexport_minimal_slots` module, * if it had a methods slot. */ - return PyLong_FromVoidPtr(&modexport_empty_slots); + return PyLong_FromVoidPtr(&modexport_minimal_slots); } static void @@ -1198,10 +1216,11 @@ PyModExport__test_from_modexport_smoke(void) static PyMethodDef methods[] = { {"get_state_int", modexport_smoke_get_state_int, METH_NOARGS}, {"get_test_token", modexport_smoke_get_test_token, METH_NOARGS}, - {"get_modexport_empty_slots", modexport_get_empty_slots, METH_NOARGS}, + {"get_modexport_minimal_slots", modexport_get_minimal_slots, METH_NOARGS}, {0}, }; static PyModuleDef_Slot slots[] = { + {Py_mod_abi, &abi_info}, {Py_mod_name, "_test_from_modexport_smoke"}, {Py_mod_doc, "the expected docstring"}, {Py_mod_exec, modexport_smoke_exec}, diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index cf49724b8861c2..0453a166c843ad 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -678,7 +678,16 @@ _grouper_next(PyObject *op) } assert(gbo->currkey != NULL); - rcmp = PyObject_RichCompareBool(igo->tgtkey, gbo->currkey, Py_EQ); + /* A user-defined __eq__ can re-enter the grouper and advance the iterator, + mutating gbo->currkey while we are comparing them. + Take local snapshots and hold strong references so INCREF/DECREF + apply to the same objects even under re-entrancy. */ + PyObject *tgtkey = Py_NewRef(igo->tgtkey); + PyObject *currkey = Py_NewRef(gbo->currkey); + rcmp = PyObject_RichCompareBool(tgtkey, currkey, Py_EQ); + Py_DECREF(tgtkey); + Py_DECREF(currkey); + if (rcmp <= 0) /* got any error or current group is end */ return NULL; diff --git a/Objects/longobject.c b/Objects/longobject.c index fde01ff302d3c2..549cf0b8f12b4e 100644 --- a/Objects/longobject.c +++ b/Objects/longobject.c @@ -188,9 +188,11 @@ long_alloc(Py_ssize_t size) _PyLong_InitTag(result); } _PyLong_SetSignAndDigitCount(result, size != 0, size); - /* The digit has to be initialized explicitly to avoid - * use-of-uninitialized-value. */ - result->long_value.ob_digit[0] = 0; +#ifdef Py_DEBUG + // gh-147988: Fill digits with an invalid pattern to catch usage + // of uninitialized digits. + memset(result->long_value.ob_digit, 0xFF, ndigits * sizeof(digit)); +#endif return result; } @@ -1097,6 +1099,7 @@ _PyLong_FromByteArray(const unsigned char* bytes, size_t n, int sign = is_signed ? -1: 1; if (idigit == 0) { sign = 0; + v->long_value.ob_digit[0] = 0; } _PyLong_SetSignAndDigitCount(v, sign, idigit); return (PyObject *)maybe_small_long(long_normalize(v)); @@ -2855,6 +2858,7 @@ long_from_non_binary_base(const char *start, const char *end, Py_ssize_t digits, *res = NULL; return 0; } + z->long_value.ob_digit[0] = 0; _PyLong_SetSignAndDigitCount(z, 0, 0); /* `convwidth` consecutive input digits are treated as a single @@ -3368,6 +3372,7 @@ x_divrem(PyLongObject *v1, PyLongObject *w1, PyLongObject **prem) *prem = NULL; return NULL; } + a->long_value.ob_digit[0] = 0; v0 = v->long_value.ob_digit; w0 = w->long_value.ob_digit; wm1 = w0[size_w-1]; @@ -4144,10 +4149,6 @@ k_mul(PyLongObject *a, PyLongObject *b) /* 1. Allocate result space. */ ret = long_alloc(asize + bsize); if (ret == NULL) goto fail; -#ifdef Py_DEBUG - /* Fill with trash, to catch reference to uninitialized digits. */ - memset(ret->long_value.ob_digit, 0xDF, _PyLong_DigitCount(ret) * sizeof(digit)); -#endif /* 2. t1 <- ah*bh, and copy into high digits of result. */ if ((t1 = k_mul(ah, bh)) == NULL) goto fail; @@ -5636,6 +5637,12 @@ long_bitwise(PyLongObject *a, Py_UNREACHABLE(); } + if ((size_z + negz) == 0) { + Py_XDECREF(new_a); + Py_XDECREF(new_b); + return get_small_int(0); + } + /* We allow an extra digit if z is negative, to make sure that the final two's complement of z doesn't overflow. */ z = long_alloc(size_z + negz); @@ -6959,6 +6966,28 @@ PyLongWriter_Finish(PyLongWriter *writer) PyLongObject *obj = (PyLongObject *)writer; assert(Py_REFCNT(obj) == 1); +#ifdef Py_DEBUG + // gh-147988: Detect uninitialized digits: long_alloc() fills digits with + // 0xFF byte pattern. It's posssible because PyLong_BASE is smaller than + // the maximum value of the C digit type (uint32_t or unsigned short): + // most significan bits are unused by the API. + Py_ssize_t ndigits = _PyLong_DigitCount(obj); + if (ndigits == 0) { + // Check ob_digit[0] digit for the number zero + ndigits = 1; + } + for (Py_ssize_t i = 0; i < ndigits; i++) { + digit d = obj->long_value.ob_digit[i]; + if (d & ~(digit)PyLong_MASK) { + Py_DECREF(obj); + PyErr_Format(PyExc_SystemError, + "PyLongWriter_Finish: digit %zd is uninitialized", + i); + return NULL; + } + } +#endif + // Normalize and get singleton if possible obj = maybe_small_long(long_normalize(obj)); diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index e3868097c0ba9f..8339e6b91a5e16 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -446,6 +446,7 @@ module_from_def_and_spec( bool seen_m_traverse_slot = false; bool seen_m_clear_slot = false; bool seen_m_free_slot = false; + bool seen_m_abi_slot = false; for (cur_slot = def_like->m_slots; cur_slot && cur_slot->slot; cur_slot++) { // Macro to copy a non-NULL, non-repeatable slot. @@ -555,6 +556,7 @@ module_from_def_and_spec( if (PyABIInfo_Check((PyABIInfo *)cur_slot->value, name) < 0) { goto error; } + seen_m_abi_slot = true; break; DEF_SLOT_CASE(Py_mod_name, char*, m_name) DEF_SLOT_CASE(Py_mod_doc, char*, m_doc) @@ -587,6 +589,14 @@ module_from_def_and_spec( #undef COPY_NONDEF_SLOT #undef COPY_NONNULL_SLOT } + if (!original_def && !seen_m_abi_slot) { + PyErr_Format( + PyExc_SystemError, + "module %s does not define Py_mod_abi," + " which is mandatory for modules defined from slots only.", + name); + goto error; + } #ifdef Py_GIL_DISABLED // For modules created directly from slots (not from a def), we enable diff --git a/Python/ceval.c b/Python/ceval.c index d4bc45f9c9130e..bf550f2da3662e 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -49,6 +49,20 @@ _Py_ReachedRecursionLimitWithMargin(PyThreadState *tstate, int margin_count) #endif } +void +_Py_EnterRecursiveCallUnchecked(PyThreadState *tstate) +{ + uintptr_t here_addr = _Py_get_machine_stack_pointer(); + _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate; +#if _Py_STACK_GROWS_DOWN + if (here_addr < _tstate->c_stack_hard_limit) { +#else + if (here_addr > _tstate->c_stack_hard_limit) { +#endif + Py_FatalError("Unchecked stack overflow."); + } +} + #if defined(__s390x__) # define Py_C_STACK_SIZE 320000 #elif defined(_WIN32) @@ -264,7 +278,7 @@ PyUnstable_ThreadState_ResetStackProtection(PyThreadState *tstate) /* The function _Py_EnterRecursiveCallTstate() only calls _Py_CheckRecursiveCall() - if the stack pointer is beyond c_stack_soft_limit. */ + if the stack pointer is between the stack base and c_stack_hard_limit. */ int _Py_CheckRecursiveCall(PyThreadState *tstate, const char *where) { @@ -273,21 +287,16 @@ _Py_CheckRecursiveCall(PyThreadState *tstate, const char *where) assert(_tstate->c_stack_soft_limit != 0); assert(_tstate->c_stack_hard_limit != 0); #if _Py_STACK_GROWS_DOWN + assert(here_addr >= _tstate->c_stack_hard_limit - _PyOS_STACK_MARGIN_BYTES); if (here_addr < _tstate->c_stack_hard_limit) { - if (here_addr < _tstate->c_stack_hard_limit - _PyOS_STACK_MARGIN_BYTES) { - // Far out of bounds -- Assume stack switching has occurred - return 0; - } + /* Overflowing while handling an overflow. Give up. */ int kbytes_used = (int)(_tstate->c_stack_top - here_addr)/1024; #else + assert(here_addr <= _tstate->c_stack_hard_limit + _PyOS_STACK_MARGIN_BYTES); if (here_addr > _tstate->c_stack_hard_limit) { - if (here_addr > _tstate->c_stack_hard_limit + _PyOS_STACK_MARGIN_BYTES) { - // Far out of bounds -- Assume stack switching has occurred - return 0; - } + /* Overflowing while handling an overflow. Give up. */ int kbytes_used = (int)(here_addr - _tstate->c_stack_top)/1024; #endif - /* Too much stack used to safely raise an exception. Give up. */ char buffer[80]; snprintf(buffer, 80, "Unrecoverable stack overflow (used %d kB)%s", kbytes_used, where); Py_FatalError(buffer); @@ -1192,6 +1201,19 @@ _PyEval_GetIter(_PyStackRef iterable, _PyStackRef *index_or_null, int yield_from return PyStackRef_FromPyObjectSteal(iter_o); } +Py_NO_INLINE int +_Py_ReachedRecursionLimit(PyThreadState *tstate) { + uintptr_t here_addr = _Py_get_machine_stack_pointer(); + _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate; + assert(_tstate->c_stack_hard_limit != 0); +#if _Py_STACK_GROWS_DOWN + return here_addr <= _tstate->c_stack_soft_limit; +#else + return here_addr >= _tstate->c_stack_soft_limit; +#endif +} + + #if (defined(__GNUC__) && __GNUC__ >= 10 && !defined(__clang__)) && defined(__x86_64__) /* * gh-129987: The SLP autovectorizer can cause poor code generation for diff --git a/Python/jit.c b/Python/jit.c index d3a40ee6ff64d5..4990c743224d3c 100644 --- a/Python/jit.c +++ b/Python/jit.c @@ -734,7 +734,7 @@ _PyJIT_Compile(_PyExecutorObject *executor, const _PyUOpInstruction trace[], siz return 0; } -/* One-off compilation of the jit entry shim. +/* One-off compilation of the jit entry shim * We compile this once only as it effectively a normal * function, but we need to use the JIT because it needs * to understand the jit-specific calling convention. diff --git a/Python/modsupport.c b/Python/modsupport.c index 4624f326d17b89..bab21d1b2be5b5 100644 --- a/Python/modsupport.c +++ b/Python/modsupport.c @@ -735,15 +735,15 @@ int PyABIInfo_Check(PyABIInfo *info, const char *module_name) return _abiinfo_raise( module_name, "incompatible future stable ABI version (%d.%d)", - ((info->abi_version) >> 24) % 0xff, - ((info->abi_version) >> 16) % 0xff); + ((info->abi_version) >> 24) & 0xff, + ((info->abi_version) >> 16) & 0xff); } if (info->abi_version < Py_PACK_VERSION(3, 2)) { return _abiinfo_raise( module_name, "invalid stable ABI version (%d.%d)", - ((info->abi_version) >> 24) % 0xff, - ((info->abi_version) >> 16) % 0xff); + ((info->abi_version) >> 24) & 0xff, + ((info->abi_version) >> 16) & 0xff); } } if (info->flags & PyABIInfo_INTERNAL) { @@ -758,8 +758,8 @@ int PyABIInfo_Check(PyABIInfo *info, const char *module_name) return _abiinfo_raise( module_name, "incompatible ABI version (%d.%d)", - ((info->abi_version) >> 24) % 0xff, - ((info->abi_version) >> 16) % 0xff); + ((info->abi_version) >> 24) & 0xff, + ((info->abi_version) >> 16) & 0xff); } } }