From 7b1b99428397230c5aefb5898bebd0902fdc8612 Mon Sep 17 00:00:00 2001 From: PhysicistJohn <54456354+PhysicistJohn@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:46:03 -0700 Subject: [PATCH 1/2] gh-154566: Fix array.byteswap() corrupting 'Zd' arrays with more than one element The 16-byte-item loop in array_array_byteswap_impl() advanced the buffer pointer by only 8 bytes per iteration, even though each iteration swaps a full 16-byte item (two independent 8-byte halves for the real/imaginary double components). This caused every item after the first to overlap the previous iteration's byte window and come out scrambled. A single byteswap() call gave wrong bytes for the second item onward. Calling byteswap() twice happened to round-trip back to the original value (the corruption is self-canceling under double application), which is why the existing test suite -- which only checked a double-call round-trip -- didn't catch it. Added a regression test that checks the actual byte-level result of a single call. --- Lib/test/test_array.py | 14 ++++++++++++++ ...-07-23-20-00-00.gh-issue-154566.byteswap-zd.rst | 6 ++++++ Modules/arraymodule.c | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-23-20-00-00.gh-issue-154566.byteswap-zd.rst diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index b5f6603defde5c..64a791dc8dcfbd 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -1611,6 +1611,20 @@ class ComplexDoubleTest(CFPTest, unittest.TestCase): typecode = 'Zd' minitemsize = 16 + def test_byteswap_single_call_result(self): + # A single byteswap() must swap each 16-byte item's two 8-byte + # halves (real, imag) independently. The pre-existing test only + # checked that byteswap() twice round-trips to the original, + # which passes even if a single call scrambles multi-item arrays. + a = array.array(self.typecode, [1+2j, 3+4j, 5+6j]) + original = a.tobytes() + a.byteswap() + expected = bytearray() + for i in range(0, len(original), 16): + item = original[i:i + 16] + expected += item[7::-1] + item[15:7:-1] + self.assertEqual(a.tobytes(), bytes(expected)) + class LargeArrayTest(unittest.TestCase): typecode = 'b' diff --git a/Misc/NEWS.d/next/Library/2026-07-23-20-00-00.gh-issue-154566.byteswap-zd.rst b/Misc/NEWS.d/next/Library/2026-07-23-20-00-00.gh-issue-154566.byteswap-zd.rst new file mode 100644 index 00000000000000..25ae8b0cb27830 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-23-20-00-00.gh-issue-154566.byteswap-zd.rst @@ -0,0 +1,6 @@ +Fix :meth:`array.array.byteswap` corrupting data for ``'Zd'`` (complex +double) arrays with more than one element: the 16-byte item loop advanced +the buffer pointer by only 8 bytes per iteration, causing items after the +first to be scrambled. A single :meth:`!byteswap` call produced incorrect +results while a double call happened to round-trip back to the original +value, which is why the existing test suite did not catch it. diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 68486c66575933..2cafb0d7b49e21 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -1617,7 +1617,7 @@ array_array_byteswap_impl(arrayobject *self) break; case 16: assert(strcmp(self->ob_descr->typecode, "Zd") == 0); - for (p = self->ob_item, i = Py_SIZE(self); --i >= 0; p += 8) { + for (p = self->ob_item, i = Py_SIZE(self); --i >= 0; p += 16) { char t0 = p[0]; char t1 = p[1]; char t2 = p[2]; From 434c62d4f85cffb1d564e6deff23740d8def57f6 Mon Sep 17 00:00:00 2001 From: PhysicistJohn <54456354+PhysicistJohn@users.noreply.github.com> Date: Fri, 24 Jul 2026 08:19:38 -0700 Subject: [PATCH 2/2] Address review feedback: trim NEWS wording, generalize the single-call test Per skirpichev's review on GH-154567: - Trim the NEWS entry to his suggested wording. - Move test_byteswap_single_call_result from ComplexDoubleTest into the shared CFPTest base class, using self.example and computing itemsize/half dynamically instead of hardcoded values, so it now covers both ComplexFloatTest ('Zf') and ComplexDoubleTest ('Zd') rather than just one. --- Lib/test/test_array.py | 30 ++++++++++--------- ...3-20-00-00.gh-issue-154566.byteswap-zd.rst | 4 +-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 64a791dc8dcfbd..0db10317df42b0 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -1569,6 +1569,22 @@ def test_byteswap(self): b.byteswap() self.assertEqual(a, b) + def test_byteswap_single_call_result(self): + # A single byteswap() must swap each item's two halves (real, + # imag) independently. test_byteswap above only checks that + # byteswap() twice round-trips to the original, which passes + # even if a single call scrambles multi-item arrays. + a = array.array(self.typecode, self.example) + original = a.tobytes() + a.byteswap() + itemsize = a.itemsize + half = itemsize // 2 + expected = bytearray() + for i in range(0, len(original), itemsize): + item = original[i:i + itemsize] + expected += item[half - 1::-1] + item[itemsize - 1:half - 1:-1] + self.assertEqual(a.tobytes(), bytes(expected)) + class HalfFloatTest(FPTest, unittest.TestCase): example = [-42.0, 0, 42, 1e2, -1e4] @@ -1611,20 +1627,6 @@ class ComplexDoubleTest(CFPTest, unittest.TestCase): typecode = 'Zd' minitemsize = 16 - def test_byteswap_single_call_result(self): - # A single byteswap() must swap each 16-byte item's two 8-byte - # halves (real, imag) independently. The pre-existing test only - # checked that byteswap() twice round-trips to the original, - # which passes even if a single call scrambles multi-item arrays. - a = array.array(self.typecode, [1+2j, 3+4j, 5+6j]) - original = a.tobytes() - a.byteswap() - expected = bytearray() - for i in range(0, len(original), 16): - item = original[i:i + 16] - expected += item[7::-1] + item[15:7:-1] - self.assertEqual(a.tobytes(), bytes(expected)) - class LargeArrayTest(unittest.TestCase): typecode = 'b' diff --git a/Misc/NEWS.d/next/Library/2026-07-23-20-00-00.gh-issue-154566.byteswap-zd.rst b/Misc/NEWS.d/next/Library/2026-07-23-20-00-00.gh-issue-154566.byteswap-zd.rst index 25ae8b0cb27830..e86b9a979946cd 100644 --- a/Misc/NEWS.d/next/Library/2026-07-23-20-00-00.gh-issue-154566.byteswap-zd.rst +++ b/Misc/NEWS.d/next/Library/2026-07-23-20-00-00.gh-issue-154566.byteswap-zd.rst @@ -1,6 +1,4 @@ Fix :meth:`array.array.byteswap` corrupting data for ``'Zd'`` (complex double) arrays with more than one element: the 16-byte item loop advanced the buffer pointer by only 8 bytes per iteration, causing items after the -first to be scrambled. A single :meth:`!byteswap` call produced incorrect -results while a double call happened to round-trip back to the original -value, which is why the existing test suite did not catch it. +first to be scrambled.