Skip to content

gh-156933: Widen narrow integer results in ctypes callbacks - #157045

Open
lazerg wants to merge 5 commits into
python:mainfrom
lazerg:fix/issue-156933-ctypes-callback-widen
Open

lazerg wants to merge 5 commits into
python:mainfrom
lazerg:fix/issue-156933-ctypes-callback-widen

Conversation

@lazerg

@lazerg lazerg commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

_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.

The fix widens narrow integer results into a register-sized buffer before writing them back, sign- or zero-extending depending on the type, replacing the old big-endian-only pointer offset that didn't actually widen anything.

Fixes #156933.

@python-cla-bot

python-cla-bot Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

@lazerg

lazerg commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

This failure is a 10 minute timeout in test_subprocess's test_check on the macOS Intel runner, unrelated to this change (which only touches _ctypes/callbacks.c).

@hugovk

This comment was marked as resolved.

@ZeroIntensity ZeroIntensity left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a test case.

@lazerg

lazerg commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

I couldn't write a test that fails before this fix and passes after it on any CI architecture. The existing test_byte/test_short/test_int callback tests in test_callbacks.py already round-trip narrow return values through a real closure call, and they pass with or without this patch on x86-64 and arm64 — those backends already read back only the bytes they need, so the missing widening never surfaces there. The corruption only shows up on an ABI where the caller trusts the full register width, which in this report is s390x, not one of the PR-gating runners, and I don't have that hardware to verify against. I'm glad to add a value-check test for the narrow int types as basic coverage if you'd still like one, though it won't reproduce the bug itself on x86-64/arm64.

@ZeroIntensity

Copy link
Copy Markdown
Member

I'm confused; how did you ensure that your fix works if you can't test on s390x? This feels a lot like you fed an issue into Codex/Claude/whatever, and you just sent the PR to us. Please don't try to fix issues that you can't even reproduce.

@ZeroIntensity ZeroIntensity added the pending The issue will be closed if no feedback is provided label Sep 20, 2026
@lazerg

lazerg commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor Author

It was AI-assisted, yes. I reviewed and drove the actual fix, and I get why the s390x question is fair.

Here's what I actually checked: the widening logic follows libffi's own contract for ffi_prep_closure_loc — narrow integer types have to fill a full ffi_arg-sized register, sign- or zero-extended. The old code just shifted a pointer for big-endian and never actually widened anything. I ran the full test_ctypes suite (628 tests) before and after on arm64, no regressions, and I also tried to reproduce the bug directly through register-priming plus a reinterpreted function pointer — it doesn't reproduce on arm64 or x86-64, because those libffi backends only read back the bytes they actually need. That's also why no portable regression test is possible on any architecture CI runs; s390x is the only one that trusts the full register.

If someone with s390x access can confirm the fix before/after, that's better evidence than anything I can produce here — happy to wait on that if you'd rather not merge on the reasoning alone.

@Endilll

Endilll commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

@miladfarca can you help testing this fix or find someone with the access to a s390x machine?

@ZeroIntensity

Copy link
Copy Markdown
Member

I really don't appreciate you copy-pasting AI output as a response. Please use your own words.

I'm going to refrain from reviewing this until someone (preferably from the LLVM team) can confirm that this is the right solution for s390x. I don't completely understand this fix, and given that you can't even reproduce the bug locally, I'm not so sure that you do either.

@miladfarca

Copy link
Copy Markdown

I have created this small test case to test this PR, it fails without the patch and passes with it on s390x, I cannot run the entire test suite as I don't have the proper environment:

diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py
index 6c7c2e52707..204eb6da301 100644
--- a/Lib/test/test_ctypes/test_callbacks.py
+++ b/Lib/test/test_ctypes/test_callbacks.py
@@ -328,6 +328,14 @@ def func():
                              f"of ctypes callback function {func!r}")
             self.assertIsNone(cm.unraisable.object)
 
+    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()

@lazerg

lazerg commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for testing this on s390x! I added your test in ebf7568. I put a 64-bit skip on it, because on 32-bit targets a long long return uses two registers and the callback only sets one. Here on arm64 macOS it passes with and without the fix, so s390x is still the only place where it catches the bug.

@lazerg

lazerg commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

I limited the test to s390x in 9338911. It failed on x86-64 CI even with the fix: libffi zero-extends a 32-bit result there, and the x86-64 ABI leaves the upper bits undefined, so reading an int result as long long is not reliable on that platform. On s390x the ABI requires the callee to extend the result, so the test is valid there and the s390x buildbots will run it.

@ZeroIntensity ZeroIntensity removed the pending The issue will be closed if no feedback is provided label Sep 27, 2026
Comment on lines +1 to +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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too technical; we want changelog entries to be meaningful to users, not to CPython developers. Let's describe what bug is actually fixed by this. Something like "Fix incorrect integer return values from ctypes callbacks on some
platforms, such as s390x."

Comment thread Modules/_ctypes/callbacks.c Outdated
Comment on lines +243 to +244
setfunc() only writes restype->size bytes. Cf. _ctypes_callproc()
in callproc.c. */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the last part of this comment:

Suggested change
setfunc() only writes restype->size bytes. Cf. _ctypes_callproc()
in callproc.c. */
setfunc() only writes restype->size bytes. */

Comment thread Modules/_ctypes/callbacks.c Outdated
widened = (ffi_arg)(ffi_sarg)*(int8_t *)&widened;
break;
case FFI_TYPE_SINT16:
widened = (ffi_arg)(ffi_sarg)*(int16_t *)&widened;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix the compiler warnings here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d54ca1f, it reads the narrow value back through a union now, so the strict-aliasing warnings are gone.

Comment thread Lib/test/test_ctypes/test_callbacks.py Outdated
Comment on lines +332 to +333
def test_narrow_int_return_widened(self):
@CFUNCTYPE(c_int)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment linking to the issue here? Something like gh-156933: Narrow integers were not widened on s390x should work.

Comment thread Lib/test/test_ctypes/test_callbacks.py Outdated
f"of ctypes callback function {func!r}")
self.assertIsNone(cm.unraisable.object)

@unittest.skipUnless(support.is_s390x, 's390x only test')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, this should run on all platforms.

Comment thread Lib/test/test_ctypes/test_callbacks.py Outdated
Comment on lines +337 to +339
addr = ctypes.cast(cb, ctypes.c_void_p).value
wide = CFUNCTYPE(c_longlong)(addr)
self.assertEqual(wide(), -1)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should come up with a test case that isn't dependent on the s390x ABI. I think we could do that by adding a function to _ctypes_test that takes a callback returning int as an argument, but returns it as a long long. Something like this:

EXPORT(long long)
_testfunc_callback_int_to_longlong(int (*func)(void))
{
    return func();
}

Then we call this via a CDLL.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in d54ca1f and it runs everywhere now. It can only fail on s390x though, since on x86-64 and arm64 the C caller extends the int to long long itself.

@ZeroIntensity

Copy link
Copy Markdown
Member

Thanks @miladfarca for confirming. I think we do have a few s390x buildbots lying around too. I'll try to run them once the review comments are addressed.

@lazerg In the future, please only work on issues that you can actually verify, and please don't use LLM output to reply to comments. I get that AI is good at solving issues like this, but it ultimately just shifts the cognitive effort to the maintainer/reviewer.

@lazerg

lazerg commented Sep 27, 2026

Copy link
Copy Markdown
Contributor Author

Yes, understood.

Comment thread Lib/test/test_ctypes/test_callbacks.py Outdated
Comment on lines +335 to +337
func = dll._testfunc_callback_int_to_longlong
func.argtypes = (CALLBACK,)
func.restype = c_longlong

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use the new wrap_dll_function util for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_ctypes fails to extend the return value per libffi contract

5 participants