diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index 6c7c2e5270736e4..d1ac62816eb8222 100644 --- a/Lib/test/test_ctypes/test_callbacks.py +++ b/Lib/test/test_ctypes/test_callbacks.py @@ -328,6 +328,21 @@ 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) + + 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(): + 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. */