From c7dcc581f55b1450608d13033a0934899d4059d6 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 6c7c2e5270736e4..c0171596a17a33e 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 000000000000000..afe1dffa96b403d --- /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 991ff0d675c2f1c..6a5b20143f55272 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 ec113e41d163238..bee29a5a3addc2c 100644 --- a/Modules/_ctypes/callbacks.c +++ b/Modules/_ctypes/callbacks.c @@ -110,6 +110,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 @@ -231,13 +247,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 @@ -248,7 +272,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 694bbd290353db70fd8a370b155cf0a6bebad58b Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 27 Sep 2026 14:48:53 -0400 Subject: [PATCH 2/2] Use CDLL directly. --- 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 c0171596a17a33e..d1ac62816eb8222 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():