From 385ed507370446178387506c455fc00079895d95 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Sun, 27 Sep 2026 23:32:18 +0500 Subject: [PATCH 1/2] gh-156933: Widen narrow integer results in `ctypes` callbacks (GH-157045) _CallPythonObject() only wrote restype->size bytes into the closure's result buffer, leaving the unused high-order bits of the ffi_arg-sized register untouched. libffi's ffi_prep_closure_loc() documents that integral types narrower than a machine register must be widened to fill it, sign-extending signed types. On architectures that always read the full register for narrow return values (s390x), this leaves garbage in the high bits, which broke libclang callbacks used by cindex.py. (cherry picked from commit b6f9a50e654dd76394bece71e6a8240ef28107ab) Co-authored-by: Lazizbek Ergashev --- Lib/test/test_ctypes/test_callbacks.py | 16 ++++- ...-09-07-00-21-31.gh-issue-156933.OalCjC.rst | 2 + Modules/_ctypes/_ctypes_test.c | 5 ++ Modules/_ctypes/callbacks.c | 67 ++++++++++++++++--- 4 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-07-00-21-31.gh-issue-156933.OalCjC.rst diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index 6c7c2e5270736e..c0171596a17a33 100644 --- a/Lib/test/test_ctypes/test_callbacks.py +++ b/Lib/test/test_ctypes/test_callbacks.py @@ -11,7 +11,7 @@ c_short, c_ushort, c_int, c_uint, c_long, c_longlong, c_ulonglong, c_ulong, c_float, c_double, c_longdouble, py_object) -from ctypes.util import find_library +from ctypes.util import find_library, wrap_dll_function from test import support from test.support import import_helper _ctypes_test = import_helper.import_module("_ctypes_test") @@ -328,6 +328,20 @@ def func(): f"of ctypes callback function {func!r}") self.assertIsNone(cm.unraisable.object) + def test_narrow_int_return_widened(self): + # gh-156933: Narrow integers were not widened on s390x + CALLBACK = CFUNCTYPE(c_int) + + @wrap_dll_function(CDLL(_ctypes_test.__file__)) + def _testfunc_callback_int_to_longlong(func: CALLBACK) -> c_longlong: + pass + + @CALLBACK + def cb(): + return -1 + + self.assertEqual(_testfunc_callback_int_to_longlong(cb), -1) + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-09-07-00-21-31.gh-issue-156933.OalCjC.rst b/Misc/NEWS.d/next/Library/2026-09-07-00-21-31.gh-issue-156933.OalCjC.rst new file mode 100644 index 00000000000000..afe1dffa96b403 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-07-00-21-31.gh-issue-156933.OalCjC.rst @@ -0,0 +1,2 @@ +Fix incorrect integer return values from :mod:`ctypes` callbacks on some +platforms, such as s390x. diff --git a/Modules/_ctypes/_ctypes_test.c b/Modules/_ctypes/_ctypes_test.c index 991ff0d675c2f1..6a5b20143f5527 100644 --- a/Modules/_ctypes/_ctypes_test.c +++ b/Modules/_ctypes/_ctypes_test.c @@ -638,6 +638,11 @@ EXPORT(long long) _testfunc_callback_q_qf(long long value, return sum; } +EXPORT(long long) _testfunc_callback_int_to_longlong(int (*func)(void)) +{ + return func(); +} + typedef struct { char *name; char *value; diff --git a/Modules/_ctypes/callbacks.c b/Modules/_ctypes/callbacks.c index fd508ae61f2e04..e67817e8f6f4d9 100644 --- a/Modules/_ctypes/callbacks.c +++ b/Modules/_ctypes/callbacks.c @@ -101,6 +101,22 @@ TryAddRef(PyObject *cnv, CDataObject *obj) } #endif +static int +is_narrow_int_ffi_type(int type) +{ + switch (type) { + case FFI_TYPE_SINT8: + case FFI_TYPE_UINT8: + case FFI_TYPE_SINT16: + case FFI_TYPE_UINT16: + case FFI_TYPE_SINT32: + case FFI_TYPE_UINT32: + return 1; + default: + return 0; + } +} + /****************************************************************************** * * Call the python object with all arguments @@ -222,13 +238,21 @@ static void _CallPythonObject(ctypes_state *st, if (restype != &ffi_type_void && result) { assert(setfunc); -#ifdef WORDS_BIGENDIAN - /* See the corresponding code in _ctypes_callproc(): - in callproc.c, around line 1219. */ - if (restype->type != FFI_TYPE_FLOAT && restype->size < sizeof(ffi_arg)) { - mem = (char *)mem + sizeof(ffi_arg) - restype->size; - } -#endif + /* libffi's closure contract requires integral results narrower + than ffi_arg to fill a whole register, sign-extended if signed; + setfunc() only writes restype->size bytes. */ + union { + ffi_arg arg; + int8_t s8; + uint8_t u8; + int16_t s16; + uint16_t u16; + int32_t s32; + uint32_t u32; + } narrow_res = {0}; + int narrow = restype->size < sizeof(ffi_arg) && + is_narrow_int_ffi_type(restype->type); + void *resmem = narrow ? (void *)&narrow_res : mem; /* keep is an object we have to keep alive so that the result stays valid. If there is no such object, the setfunc will @@ -239,7 +263,34 @@ static void _CallPythonObject(ctypes_state *st, be the result. EXCEPT when restype is py_object - Python itself knows how to manage the refcount of these objects. */ - PyObject *keep = setfunc(mem, result, restype->size); + PyObject *keep = setfunc(resmem, result, restype->size); + + if (narrow && keep != NULL) { + ffi_arg widened; + switch (restype->type) { + case FFI_TYPE_SINT8: + widened = (ffi_arg)(ffi_sarg)narrow_res.s8; + break; + case FFI_TYPE_SINT16: + widened = (ffi_arg)(ffi_sarg)narrow_res.s16; + break; + case FFI_TYPE_SINT32: + widened = (ffi_arg)(ffi_sarg)narrow_res.s32; + break; + case FFI_TYPE_UINT8: + widened = narrow_res.u8; + break; + case FFI_TYPE_UINT16: + widened = narrow_res.u16; + break; + case FFI_TYPE_UINT32: + widened = narrow_res.u32; + break; + default: + Py_UNREACHABLE(); + } + memcpy(mem, &widened, sizeof(ffi_arg)); + } if (keep == NULL) { /* Could not convert callback result. */ From 7fb46159434526f6c8ba8199a7bcdc80daa6a286 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 27 Sep 2026 14:46:13 -0400 Subject: [PATCH 2/2] Use the boring old way of creating function pointers. --- Lib/test/test_ctypes/test_callbacks.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index c0171596a17a33..d1ac62816eb822 100644 --- a/Lib/test/test_ctypes/test_callbacks.py +++ b/Lib/test/test_ctypes/test_callbacks.py @@ -11,7 +11,7 @@ c_short, c_ushort, c_int, c_uint, c_long, c_longlong, c_ulonglong, c_ulong, c_float, c_double, c_longdouble, py_object) -from ctypes.util import find_library, wrap_dll_function +from ctypes.util import find_library from test import support from test.support import import_helper _ctypes_test = import_helper.import_module("_ctypes_test") @@ -332,9 +332,10 @@ def test_narrow_int_return_widened(self): # gh-156933: Narrow integers were not widened on s390x CALLBACK = CFUNCTYPE(c_int) - @wrap_dll_function(CDLL(_ctypes_test.__file__)) - def _testfunc_callback_int_to_longlong(func: CALLBACK) -> c_longlong: - pass + dll = CDLL(_ctypes_test.__file__) + _testfunc_callback_int_to_longlong = dll._testfunc_callback_int_to_longlong + _testfunc_callback_int_to_longlong.argtypes = [CALLBACK] + _testfunc_callback_int_to_longlong.restype = c_longlong @CALLBACK def cb():