From fbcec5a7ffe7efdc5fce079e39cf638adc3586b5 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 13 Mar 2026 10:43:32 +0300 Subject: [PATCH 1/4] gh-143050: correct PyLong_FromString() to use _PyLong_Negate() The long_from_string_base() might return a small integer, when the _pylong.py is used to do conversion. Hence, we must be careful here to not smash it "small int" bit by using the _PyLong_FlipSign(). --- Lib/test/test_capi/test_long.py | 12 +++++++++ Modules/Setup.stdlib.in | 2 +- Modules/_testinternalcapi.c | 3 +++ Modules/_testinternalcapi/long.c | 33 +++++++++++++++++++++++ Modules/_testinternalcapi/parts.h | 1 + Objects/longobject.c | 6 ++--- PCbuild/_testinternalcapi.vcxproj | 1 + PCbuild/_testinternalcapi.vcxproj.filters | 3 +++ 8 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 Modules/_testinternalcapi/long.c diff --git a/Lib/test/test_capi/test_long.py b/Lib/test/test_capi/test_long.py index d3156645eeec2d..07dcf979feff54 100644 --- a/Lib/test/test_capi/test_long.py +++ b/Lib/test/test_capi/test_long.py @@ -803,6 +803,18 @@ def to_digits(num): self.assertEqual(pylongwriter_create(negative, digits), num, (negative, digits)) + def test_bug_143050(self): + _testinternalcapi = import_helper.import_module('_testinternalcapi') + _pylong_is_small_int = _testinternalcapi._pylong_is_small_int + + self.assertRaises(TypeError, _pylong_is_small_int, 1j) + + with support.adjust_int_max_str_digits(0): + self.assertTrue(_testinternalcapi._pylong_is_small_int(0)) + a = int('-' + '0' * 7000, 10) + del a + self.assertTrue(_testinternalcapi._pylong_is_small_int(0)) + if __name__ == "__main__": unittest.main() diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 0d520684c795d6..3b8bd69c35b243 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -174,7 +174,7 @@ @MODULE_XXSUBTYPE_TRUE@xxsubtype xxsubtype.c @MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c @MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c -@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tuple.c +@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tuple.c _testinternalcapi/long.c @MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c @MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index aa5911ef2fb449..41fd049ed867ff 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2990,6 +2990,9 @@ module_exec(PyObject *module) if (_PyTestInternalCapi_Init_Tuple(module) < 0) { return 1; } + if (_PyTestInternalCapi_Init_Long(module) < 0) { + return 1; + } Py_ssize_t sizeof_gc_head = 0; #ifndef Py_GIL_DISABLED diff --git a/Modules/_testinternalcapi/long.c b/Modules/_testinternalcapi/long.c new file mode 100644 index 00000000000000..b3e7c31ad7a7c5 --- /dev/null +++ b/Modules/_testinternalcapi/long.c @@ -0,0 +1,33 @@ +#include "parts.h" +#include "../_testcapi/util.h" + +#define Py_BUILD_CORE +#include "pycore_long.h" + + +static PyObject * +_pylong_is_small_int(PyObject *Py_UNUSED(module), PyObject *arg) +{ + NULLABLE(arg); + if (!PyLong_CheckExact(arg)) { + PyErr_SetString(PyExc_TypeError, "arg must be int"); + return NULL; + } + return PyBool_FromLong(((PyLongObject *)arg)->long_value.lv_tag + & IMMORTALITY_BIT_MASK); +} + +static PyMethodDef test_methods[] = { + {"_pylong_is_small_int", _pylong_is_small_int, METH_O}, + {NULL}, +}; + +int +_PyTestInternalCapi_Init_Long(PyObject *mod) +{ + if (PyModule_AddFunctions(mod, test_methods) < 0) { + return -1; + } + + return 0; +} diff --git a/Modules/_testinternalcapi/parts.h b/Modules/_testinternalcapi/parts.h index 81f536c3babb18..a0613a316a10a7 100644 --- a/Modules/_testinternalcapi/parts.h +++ b/Modules/_testinternalcapi/parts.h @@ -16,5 +16,6 @@ int _PyTestInternalCapi_Init_Set(PyObject *module); int _PyTestInternalCapi_Init_Complex(PyObject *module); int _PyTestInternalCapi_Init_CriticalSection(PyObject *module); int _PyTestInternalCapi_Init_Tuple(PyObject *module); +int _PyTestInternalCapi_Init_Long(PyObject *module); #endif // Py_TESTINTERNALCAPI_PARTS_H diff --git a/Objects/longobject.c b/Objects/longobject.c index 7ce5d0535b884e..08647d206262c0 100644 --- a/Objects/longobject.c +++ b/Objects/longobject.c @@ -3118,11 +3118,11 @@ PyLong_FromString(const char *str, char **pend, int base) } /* Set sign and normalize */ - if (sign < 0) { - _PyLong_FlipSign(z); - } long_normalize(z); z = maybe_small_long(z); + if (sign < 0) { + _PyLong_Negate(&z); + } if (pend != NULL) { *pend = (char *)str; diff --git a/PCbuild/_testinternalcapi.vcxproj b/PCbuild/_testinternalcapi.vcxproj index f3e423fa04668e..c54c4d8c034e50 100644 --- a/PCbuild/_testinternalcapi.vcxproj +++ b/PCbuild/_testinternalcapi.vcxproj @@ -101,6 +101,7 @@ + diff --git a/PCbuild/_testinternalcapi.vcxproj.filters b/PCbuild/_testinternalcapi.vcxproj.filters index 7ab242c2c230b6..c3973ebb6ec647 100644 --- a/PCbuild/_testinternalcapi.vcxproj.filters +++ b/PCbuild/_testinternalcapi.vcxproj.filters @@ -30,6 +30,9 @@ Source Files + + Source Files + From b8333ba67da4773f79e668495b7352ed2650c41b Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 13 Mar 2026 12:47:13 +0300 Subject: [PATCH 2/4] cleanup, use _testcapi.test_immortal_small_ints() --- Lib/test/test_capi/test_long.py | 8 +----- Modules/Setup.stdlib.in | 2 +- Modules/_testinternalcapi.c | 3 --- Modules/_testinternalcapi/long.c | 33 ----------------------- Modules/_testinternalcapi/parts.h | 1 - PCbuild/_testinternalcapi.vcxproj | 1 - PCbuild/_testinternalcapi.vcxproj.filters | 3 --- 7 files changed, 2 insertions(+), 49 deletions(-) delete mode 100644 Modules/_testinternalcapi/long.c diff --git a/Lib/test/test_capi/test_long.py b/Lib/test/test_capi/test_long.py index 07dcf979feff54..af82710d31ebfc 100644 --- a/Lib/test/test_capi/test_long.py +++ b/Lib/test/test_capi/test_long.py @@ -804,16 +804,10 @@ def to_digits(num): (negative, digits)) def test_bug_143050(self): - _testinternalcapi = import_helper.import_module('_testinternalcapi') - _pylong_is_small_int = _testinternalcapi._pylong_is_small_int - - self.assertRaises(TypeError, _pylong_is_small_int, 1j) - with support.adjust_int_max_str_digits(0): - self.assertTrue(_testinternalcapi._pylong_is_small_int(0)) a = int('-' + '0' * 7000, 10) del a - self.assertTrue(_testinternalcapi._pylong_is_small_int(0)) + _testcapi.test_immortal_small_ints() if __name__ == "__main__": diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 3b8bd69c35b243..0d520684c795d6 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -174,7 +174,7 @@ @MODULE_XXSUBTYPE_TRUE@xxsubtype xxsubtype.c @MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c @MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c -@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tuple.c _testinternalcapi/long.c +@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tuple.c @MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c @MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 41fd049ed867ff..aa5911ef2fb449 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2990,9 +2990,6 @@ module_exec(PyObject *module) if (_PyTestInternalCapi_Init_Tuple(module) < 0) { return 1; } - if (_PyTestInternalCapi_Init_Long(module) < 0) { - return 1; - } Py_ssize_t sizeof_gc_head = 0; #ifndef Py_GIL_DISABLED diff --git a/Modules/_testinternalcapi/long.c b/Modules/_testinternalcapi/long.c deleted file mode 100644 index b3e7c31ad7a7c5..00000000000000 --- a/Modules/_testinternalcapi/long.c +++ /dev/null @@ -1,33 +0,0 @@ -#include "parts.h" -#include "../_testcapi/util.h" - -#define Py_BUILD_CORE -#include "pycore_long.h" - - -static PyObject * -_pylong_is_small_int(PyObject *Py_UNUSED(module), PyObject *arg) -{ - NULLABLE(arg); - if (!PyLong_CheckExact(arg)) { - PyErr_SetString(PyExc_TypeError, "arg must be int"); - return NULL; - } - return PyBool_FromLong(((PyLongObject *)arg)->long_value.lv_tag - & IMMORTALITY_BIT_MASK); -} - -static PyMethodDef test_methods[] = { - {"_pylong_is_small_int", _pylong_is_small_int, METH_O}, - {NULL}, -}; - -int -_PyTestInternalCapi_Init_Long(PyObject *mod) -{ - if (PyModule_AddFunctions(mod, test_methods) < 0) { - return -1; - } - - return 0; -} diff --git a/Modules/_testinternalcapi/parts.h b/Modules/_testinternalcapi/parts.h index a0613a316a10a7..81f536c3babb18 100644 --- a/Modules/_testinternalcapi/parts.h +++ b/Modules/_testinternalcapi/parts.h @@ -16,6 +16,5 @@ int _PyTestInternalCapi_Init_Set(PyObject *module); int _PyTestInternalCapi_Init_Complex(PyObject *module); int _PyTestInternalCapi_Init_CriticalSection(PyObject *module); int _PyTestInternalCapi_Init_Tuple(PyObject *module); -int _PyTestInternalCapi_Init_Long(PyObject *module); #endif // Py_TESTINTERNALCAPI_PARTS_H diff --git a/PCbuild/_testinternalcapi.vcxproj b/PCbuild/_testinternalcapi.vcxproj index c54c4d8c034e50..f3e423fa04668e 100644 --- a/PCbuild/_testinternalcapi.vcxproj +++ b/PCbuild/_testinternalcapi.vcxproj @@ -101,7 +101,6 @@ - diff --git a/PCbuild/_testinternalcapi.vcxproj.filters b/PCbuild/_testinternalcapi.vcxproj.filters index c3973ebb6ec647..7ab242c2c230b6 100644 --- a/PCbuild/_testinternalcapi.vcxproj.filters +++ b/PCbuild/_testinternalcapi.vcxproj.filters @@ -30,9 +30,6 @@ Source Files - - Source Files - From 20e79c6c3cc5c8a152d2962e98220e30e68ce2dd Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 14 Mar 2026 03:02:02 +0300 Subject: [PATCH 3/4] + add also test with nonzero small int --- Lib/test/test_capi/test_long.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_capi/test_long.py b/Lib/test/test_capi/test_long.py index af82710d31ebfc..adb5b545d52b48 100644 --- a/Lib/test/test_capi/test_long.py +++ b/Lib/test/test_capi/test_long.py @@ -805,8 +805,9 @@ def to_digits(num): def test_bug_143050(self): with support.adjust_int_max_str_digits(0): - a = int('-' + '0' * 7000, 10) - del a + int('-' + '0' * 7000, 10) + _testcapi.test_immortal_small_ints() + int('-' + '0' * 7000 + '123', 10) _testcapi.test_immortal_small_ints() From 413e56d52f3a0717f4abe2481287700759f1e250 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 14 Mar 2026 03:46:38 +0300 Subject: [PATCH 4/4] _long_is_small_int() -> _PyLong_IsImmortal() helper --- Include/internal/pycore_long.h | 13 ++++++++++++- Modules/_testcapi/immortal.c | 4 ++-- Objects/longobject.c | 14 ++------------ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Include/internal/pycore_long.h b/Include/internal/pycore_long.h index d545ba0c3abb52..376c55eb09869a 100644 --- a/Include/internal/pycore_long.h +++ b/Include/internal/pycore_long.h @@ -231,6 +231,15 @@ _PyLong_IsPositive(const PyLongObject *op) return (op->long_value.lv_tag & SIGN_MASK) == 0; } +static inline bool +_PyLong_IsImmortal(const PyLongObject *op) +{ + assert(PyLong_Check(op)); + bool is_small_int = (op->long_value.lv_tag & IMMORTALITY_BIT_MASK) != 0; + assert(PyLong_CheckExact(op) || (!is_small_int)); + return is_small_int; +} + static inline Py_ssize_t _PyLong_DigitCount(const PyLongObject *op) { @@ -292,7 +301,9 @@ _PyLong_SetDigitCount(PyLongObject *op, Py_ssize_t size) #define NON_SIZE_MASK ~(uintptr_t)((1 << NON_SIZE_BITS) - 1) static inline void -_PyLong_FlipSign(PyLongObject *op) { +_PyLong_FlipSign(PyLongObject *op) +{ + assert(!_PyLong_IsImmortal(op)); unsigned int flipped_sign = 2 - (op->long_value.lv_tag & SIGN_MASK); op->long_value.lv_tag &= NON_SIZE_MASK; op->long_value.lv_tag |= flipped_sign; diff --git a/Modules/_testcapi/immortal.c b/Modules/_testcapi/immortal.c index af510cab655356..21c6cdfb9a2cb2 100644 --- a/Modules/_testcapi/immortal.c +++ b/Modules/_testcapi/immortal.c @@ -31,13 +31,13 @@ test_immortal_small_ints(PyObject *self, PyObject *Py_UNUSED(ignored)) for (int i = -5; i <= 1024; i++) { PyObject *obj = PyLong_FromLong(i); assert(verify_immortality(obj)); - int has_int_immortal_bit = ((PyLongObject *)obj)->long_value.lv_tag & IMMORTALITY_BIT_MASK; + int has_int_immortal_bit = _PyLong_IsImmortal((PyLongObject *)obj); assert(has_int_immortal_bit); } for (int i = 1025; i <= 1030; i++) { PyObject *obj = PyLong_FromLong(i); assert(obj); - int has_int_immortal_bit = ((PyLongObject *)obj)->long_value.lv_tag & IMMORTALITY_BIT_MASK; + int has_int_immortal_bit = _PyLong_IsImmortal((PyLongObject *)obj); assert(!has_int_immortal_bit); Py_DECREF(obj); } diff --git a/Objects/longobject.c b/Objects/longobject.c index 08647d206262c0..35f17198ee1264 100644 --- a/Objects/longobject.c +++ b/Objects/longobject.c @@ -3622,21 +3622,11 @@ long_richcompare(PyObject *self, PyObject *other, int op) Py_RETURN_RICHCOMPARE(result, 0, op); } -static inline int -/// Return 1 if the object is one of the immortal small ints -_long_is_small_int(PyObject *op) -{ - PyLongObject *long_object = (PyLongObject *)op; - int is_small_int = (long_object->long_value.lv_tag & IMMORTALITY_BIT_MASK) != 0; - assert((!is_small_int) || PyLong_CheckExact(op)); - return is_small_int; -} - void _PyLong_ExactDealloc(PyObject *self) { assert(PyLong_CheckExact(self)); - if (_long_is_small_int(self)) { + if (_PyLong_IsImmortal((PyLongObject *)self)) { // See PEP 683, section Accidental De-Immortalizing for details _Py_SetImmortal(self); return; @@ -3651,7 +3641,7 @@ _PyLong_ExactDealloc(PyObject *self) static void long_dealloc(PyObject *self) { - if (_long_is_small_int(self)) { + if (_PyLong_IsImmortal((PyLongObject *)self)) { /* This should never get called, but we also don't want to SEGV if * we accidentally decref small Ints out of existence. Instead, * since small Ints are immortal, re-set the reference count.