https://github.com/python/cpython/commit/b6f9a50e654dd76394bece71e6a8240ef28107ab
commit: b6f9a50e654dd76394bece71e6a8240ef28107ab
branch: main
author: Lazizbek Ergashev <[email protected]>
committer: ZeroIntensity <[email protected]>
date: 2026-09-27T14:32:18-04:00
summary:
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.
files:
A Misc/NEWS.d/next/Library/2026-09-07-00-21-31.gh-issue-156933.OalCjC.rst
M Lib/test/test_ctypes/test_callbacks.py
M Modules/_ctypes/_ctypes_test.c
M Modules/_ctypes/callbacks.c
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 fd508ae61f2e040..e67817e8f6f4d96 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. */
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]