Skip to content

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

Merged
ZeroIntensity merged 6 commits into
python:mainfrom
lazerg:fix/issue-156933-ctypes-callback-widen
Sep 27, 2026
Merged

ZeroIntensity merged 6 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 thread Misc/NEWS.d/next/Library/2026-09-07-00-21-31.gh-issue-156933.OalCjC.rst Outdated
Comment thread Modules/_ctypes/callbacks.c Outdated
Comment thread Modules/_ctypes/callbacks.c Outdated
Comment thread Lib/test/test_ctypes/test_callbacks.py Outdated
Comment thread Lib/test/test_ctypes/test_callbacks.py Outdated
Comment thread Lib/test/test_ctypes/test_callbacks.py Outdated
@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
@ZeroIntensity

Copy link
Copy Markdown
Member

!buildbot s390x

@bedevere-bot

Copy link
Copy Markdown

🤖 New build scheduled with the buildbot fleet by @ZeroIntensity for commit 5693c85 🤖

Results will be shown at:

https://buildbot.python.org/all/#/grid?branch=refs%2Fpull%2F157045%2Fmerge

The command will test the builders whose names match following regular expression: s390x

The builders matched are:

  • s390x CentOS10 Clang Installed PR
  • s390x Fedora Stable LTO PR
  • s390x Fedora Stable Clang Installed PR
  • s390x Fedora Rawhide NoGIL refleaks PR
  • s390x Fedora Stable LTO + PGO PR
  • s390x Fedora Rawhide LTO + PGO PR
  • s390x CentOS10 LTO PR
  • s390x Fedora Rawhide Clang PR
  • s390x RHEL9 LTO PR
  • s390x Fedora Stable PR
  • s390x CentOS10 Clang PR
  • s390x Fedora Rawhide Clang Installed PR
  • s390x RHEL9 PR
  • s390x CentOS10 Refleaks PR
  • s390x RHEL9 LTO + PGO PR
  • s390x CentOS10 PR
  • s390x Fedora Stable Refleaks PR
  • s390x Fedora Rawhide NoGIL PR
  • s390x Fedora Rawhide PR
  • s390x CentOS10 LTO + PGO PR
  • s390x Fedora Rawhide LTO PR
  • s390x Fedora Rawhide Refleaks PR
  • s390x RHEL9 Refleaks PR
  • s390x Fedora Stable Clang PR

@ZeroIntensity ZeroIntensity added needs backport to 3.14 bugs and security fixes needs backport to 3.15 pre-release feature fixes, bugs and security fixes labels Sep 27, 2026

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

Thanks, this looks good. The buildbot failures on Fedora look unrelated; I've opened an issue: #158294.

@ZeroIntensity
ZeroIntensity merged commit b6f9a50 into python:main Sep 27, 2026
79 of 85 checks passed
@miss-islington-app

Copy link
Copy Markdown

Thanks @lazerg for the PR, and @ZeroIntensity for merging it 🌮🎉.. I'm working now to backport this PR to: 3.13, 3.14, 3.15.
🐍🍒⛏🤖

@bedevere-app

bedevere-app Bot commented Sep 27, 2026

Copy link
Copy Markdown

GH-158295 is a backport of this pull request to the 3.15 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.15 pre-release feature fixes, bugs and security fixes label Sep 27, 2026
@miss-islington-app

Copy link
Copy Markdown

Sorry, @lazerg and @ZeroIntensity, I could not cleanly backport this to 3.13 due to a conflict.
Please backport using cherry_picker on command line.

cherry_picker b6f9a50e654dd76394bece71e6a8240ef28107ab 3.13

@bedevere-app

bedevere-app Bot commented Sep 27, 2026

Copy link
Copy Markdown

GH-158296 is a backport of this pull request to the 3.14 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.14 bugs and security fixes label Sep 27, 2026
@bedevere-app

bedevere-app Bot commented Sep 27, 2026

Copy link
Copy Markdown

GH-158297 is a backport of this pull request to the 3.13 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.13 bugs and security fixes label Sep 27, 2026
ZeroIntensity added a commit that referenced this pull request Sep 27, 2026
…H-157045) (GH-158297)

_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 b6f9a50)

Co-authored-by: Lazizbek Ergashev <lazerg2@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_ctypes fails to extend the return value per libffi contract

6 participants