From 72a218b93a538cfe0bf079a74f03401a504c178f Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Mon, 7 Sep 2026 00:28:35 +0500 Subject: [PATCH 1/5] gh-156933: Widen narrow integer results in ctypes callbacks --- ...-09-07-00-21-31.gh-issue-156933.OalCjC.rst | 3 + Modules/_ctypes/callbacks.c | 59 ++++++++++++++++--- 2 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-07-00-21-31.gh-issue-156933.OalCjC.rst 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..27a7c83dc46dde7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-07-00-21-31.gh-issue-156933.OalCjC.rst @@ -0,0 +1,3 @@ +Fix :mod:`ctypes` callbacks returning an integer narrower than a machine +register: the result is now widened to fill the register, as libffi's +closure contract requires. diff --git a/Modules/_ctypes/callbacks.c b/Modules/_ctypes/callbacks.c index fd508ae61f2e040..ddaf16ea494f202 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,14 @@ 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. Cf. _ctypes_callproc() + in callproc.c. */ + ffi_arg widened = 0; + int narrow = restype->size < sizeof(ffi_arg) && + is_narrow_int_ffi_type(restype->type); + void *resmem = narrow ? &widened : 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 +256,33 @@ 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) { + switch (restype->type) { + case FFI_TYPE_SINT8: + widened = (ffi_arg)(ffi_sarg)*(int8_t *)&widened; + break; + case FFI_TYPE_SINT16: + widened = (ffi_arg)(ffi_sarg)*(int16_t *)&widened; + break; + case FFI_TYPE_SINT32: + widened = (ffi_arg)(ffi_sarg)*(int32_t *)&widened; + break; + case FFI_TYPE_UINT8: + widened = *(uint8_t *)&widened; + break; + case FFI_TYPE_UINT16: + widened = *(uint16_t *)&widened; + break; + case FFI_TYPE_UINT32: + widened = *(uint32_t *)&widened; + break; + default: + break; + } + memcpy(mem, &widened, sizeof(ffi_arg)); + } if (keep == NULL) { /* Could not convert callback result. */ From ebf756899ec9383fd99a02a71cedb06b16ac79c6 Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 25 Sep 2026 20:18:13 +0500 Subject: [PATCH 2/5] Add test for widened narrow int callback results --- Lib/test/test_ctypes/test_callbacks.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index 6c7c2e5270736e4..c56c15ee36e46a8 100644 --- a/Lib/test/test_ctypes/test_callbacks.py +++ b/Lib/test/test_ctypes/test_callbacks.py @@ -328,6 +328,16 @@ def func(): f"of ctypes callback function {func!r}") self.assertIsNone(cm.unraisable.object) + @unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform') + def test_narrow_int_return_widened(self): + @CFUNCTYPE(c_int) + def cb(): + return -1 + + addr = ctypes.cast(cb, ctypes.c_void_p).value + wide = CFUNCTYPE(c_longlong)(addr) + self.assertEqual(wide(), -1) + if __name__ == '__main__': unittest.main() From 933891109a29e37a10937606e44cf779bd82ac57 Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 25 Sep 2026 20:52:43 +0500 Subject: [PATCH 3/5] Only run the widening test on s390x --- Lib/test/test_ctypes/test_callbacks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index c56c15ee36e46a8..108885d66c558ae 100644 --- a/Lib/test/test_ctypes/test_callbacks.py +++ b/Lib/test/test_ctypes/test_callbacks.py @@ -328,7 +328,7 @@ def func(): f"of ctypes callback function {func!r}") self.assertIsNone(cm.unraisable.object) - @unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform') + @unittest.skipUnless(support.is_s390x, 's390x only test') def test_narrow_int_return_widened(self): @CFUNCTYPE(c_int) def cb(): From d54ca1f6f1f0c4de4b99abe5ce2ee29e84377ae9 Mon Sep 17 00:00:00 2001 From: lazerg Date: Sun, 27 Sep 2026 07:56:59 +0500 Subject: [PATCH 4/5] Address review comments --- Lib/test/test_ctypes/test_callbacks.py | 14 +++++---- ...-09-07-00-21-31.gh-issue-156933.OalCjC.rst | 5 ++-- Modules/_ctypes/_ctypes_test.c | 5 ++++ Modules/_ctypes/callbacks.c | 30 ++++++++++++------- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index 108885d66c558ae..b7f97aa1b2ab609 100644 --- a/Lib/test/test_ctypes/test_callbacks.py +++ b/Lib/test/test_ctypes/test_callbacks.py @@ -328,15 +328,19 @@ def func(): f"of ctypes callback function {func!r}") self.assertIsNone(cm.unraisable.object) - @unittest.skipUnless(support.is_s390x, 's390x only test') def test_narrow_int_return_widened(self): - @CFUNCTYPE(c_int) + # gh-156933: Narrow integers were not widened on s390x + dll = CDLL(_ctypes_test.__file__) + CALLBACK = CFUNCTYPE(c_int) + func = dll._testfunc_callback_int_to_longlong + func.argtypes = (CALLBACK,) + func.restype = c_longlong + + @CALLBACK def cb(): return -1 - addr = ctypes.cast(cb, ctypes.c_void_p).value - wide = CFUNCTYPE(c_longlong)(addr) - self.assertEqual(wide(), -1) + self.assertEqual(func(cb), -1) if __name__ == '__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 index 27a7c83dc46dde7..afe1dffa96b403d 100644 --- 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 @@ -1,3 +1,2 @@ -Fix :mod:`ctypes` callbacks returning an integer narrower than a machine -register: the result is now widened to fill the register, as libffi's -closure contract requires. +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 ddaf16ea494f202..e67817e8f6f4d96 100644 --- a/Modules/_ctypes/callbacks.c +++ b/Modules/_ctypes/callbacks.c @@ -240,12 +240,19 @@ static void _CallPythonObject(ctypes_state *st, /* 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. Cf. _ctypes_callproc() - in callproc.c. */ - ffi_arg widened = 0; + 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 ? &widened : mem; + 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 @@ -259,27 +266,28 @@ static void _CallPythonObject(ctypes_state *st, 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)*(int8_t *)&widened; + widened = (ffi_arg)(ffi_sarg)narrow_res.s8; break; case FFI_TYPE_SINT16: - widened = (ffi_arg)(ffi_sarg)*(int16_t *)&widened; + widened = (ffi_arg)(ffi_sarg)narrow_res.s16; break; case FFI_TYPE_SINT32: - widened = (ffi_arg)(ffi_sarg)*(int32_t *)&widened; + widened = (ffi_arg)(ffi_sarg)narrow_res.s32; break; case FFI_TYPE_UINT8: - widened = *(uint8_t *)&widened; + widened = narrow_res.u8; break; case FFI_TYPE_UINT16: - widened = *(uint16_t *)&widened; + widened = narrow_res.u16; break; case FFI_TYPE_UINT32: - widened = *(uint32_t *)&widened; + widened = narrow_res.u32; break; default: - break; + Py_UNREACHABLE(); } memcpy(mem, &widened, sizeof(ffi_arg)); } From 6490b5f46e77593791142e4f8dd5623abd5f562b Mon Sep 17 00:00:00 2001 From: lazerg Date: Sun, 27 Sep 2026 08:26:38 +0500 Subject: [PATCH 5/5] Use wrap_dll_function in the widening test --- Lib/test/test_ctypes/test_callbacks.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index b7f97aa1b2ab609..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") @@ -330,17 +330,17 @@ def func(): def test_narrow_int_return_widened(self): # gh-156933: Narrow integers were not widened on s390x - dll = CDLL(_ctypes_test.__file__) CALLBACK = CFUNCTYPE(c_int) - func = dll._testfunc_callback_int_to_longlong - func.argtypes = (CALLBACK,) - func.restype = c_longlong + + @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(func(cb), -1) + self.assertEqual(_testfunc_callback_int_to_longlong(cb), -1) if __name__ == '__main__':