From eba13d8019c43ef09b22678ba0e40eeb1f70191d Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 25 Mar 2026 14:58:06 +0100 Subject: [PATCH 1/4] gh-145633: Fix struct.pack('f') on s390x Use PyFloat_Pack4() to raise OverflowError. Add more tests on packing/unpacking floats. --- Lib/test/test_struct.py | 36 ++++++++++++++++++++++++++++-------- Modules/_struct.c | 5 ++--- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index c7dc69defded50..1c5004bc2b9f52 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -365,7 +365,7 @@ def test_p_code(self): (got,) = struct.unpack(code, got) self.assertEqual(got, expectedback) - def test_705836(self): + def check_705836(self, format, reverse_format): # SF bug 705836. "f" had a severe rounding bug, where a carry # from the low-order discarded bits could propagate into the exponent # field, causing the result to be wrong by a factor of 2. @@ -376,27 +376,33 @@ def test_705836(self): delta /= 2.0 smaller = base - delta # Packing this rounds away a solid string of trailing 1 bits. - packed = struct.pack("f", smaller) + + bigpacked = struct.pack(reverse_format, smaller) self.assertEqual(bigpacked, string_reverse(packed)) - unpacked = struct.unpack(">f", bigpacked)[0] + unpacked = struct.unpack(reverse_format, bigpacked)[0] self.assertEqual(base, unpacked) # Largest finite IEEE single. big = (1 << 24) - 1 big = math.ldexp(big, 127 - 23) - packed = struct.pack(">f", big) - unpacked = struct.unpack(">f", packed)[0] + packed = struct.pack(format, big) + unpacked = struct.unpack(format, packed)[0] self.assertEqual(big, unpacked) # The same, but tack on a 1 bit so it rounds up to infinity. big = (1 << 25) - 1 big = math.ldexp(big, 127 - 24) - self.assertRaises(OverflowError, struct.pack, ">f", big) + self.assertRaises(OverflowError, struct.pack, format, big) + + def test_705836(self): + self.check_705836("f") + self.check_705836(">f", "f") def test_1530559(self): for code, byteorder in iter_integer_formats(): @@ -1201,6 +1207,20 @@ def test_half_float(self): for formatcode, bits, f in format_bits_float__doubleRoundingError_list: self.assertEqual(bits, struct.pack(formatcode, f)) + def test_float_round_trip(self): + for format in ("f", "f", "d", "d"): + with self.subTest(format=format): + f = struct.unpack(format, struct.pack(format, 1.5))[0] + self.assertEqual(f, 1.5) + f = struct.unpack(format, struct.pack(format, NAN))[0] + self.assertTrue(math.isnan(f), f) + f = struct.unpack(format, struct.pack(format, INF))[0] + self.assertTrue(math.isinf(f), f) + self.assertEqual(math.copysign(1.0, f), 1.0) + f = struct.unpack(format, struct.pack(format, -INF))[0] + self.assertTrue(math.isinf(f), f) + self.assertEqual(math.copysign(1.0, f), -1.0) + if __name__ == '__main__': unittest.main() diff --git a/Modules/_struct.c b/Modules/_struct.c index d6995895c2b9e3..c235e27a415543 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -763,14 +763,13 @@ np_halffloat(_structmodulestate *state, char *p, PyObject *v, const formatdef *f static int np_float(_structmodulestate *state, char *p, PyObject *v, const formatdef *f) { - float x = (float)PyFloat_AsDouble(v); + double x = PyFloat_AsDouble(v); if (x == -1 && PyErr_Occurred()) { PyErr_SetString(state->StructError, "required argument is not a float"); return -1; } - memcpy(p, &x, sizeof x); - return 0; + return PyFloat_Pack4(x, p, PY_LITTLE_ENDIAN); } static int From ce1093bd0cf131dd5e765141b5cc6627f7bc4a6b Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 26 Mar 2026 01:58:58 +0100 Subject: [PATCH 2/4] Rewrite test_705836() Co-authored-by: Sergey B Kirpichev --- Lib/test/test_struct.py | 51 +++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index 1c5004bc2b9f52..2942ae66f2b0e1 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -365,7 +365,7 @@ def test_p_code(self): (got,) = struct.unpack(code, got) self.assertEqual(got, expectedback) - def check_705836(self, format, reverse_format): + def test_705836(self): # SF bug 705836. "f" had a severe rounding bug, where a carry # from the low-order discarded bits could propagate into the exponent # field, causing the result to be wrong by a factor of 2. @@ -376,33 +376,42 @@ def check_705836(self, format, reverse_format): delta /= 2.0 smaller = base - delta # Packing this rounds away a solid string of trailing 1 bits. - packed = struct.pack(format, smaller) - unpacked = struct.unpack(format, packed)[0] + packed = struct.pack("f", smaller) self.assertEqual(bigpacked, string_reverse(packed)) - unpacked = struct.unpack(reverse_format, bigpacked)[0] + unpacked = struct.unpack(">f", bigpacked)[0] self.assertEqual(base, unpacked) # Largest finite IEEE single. big = (1 << 24) - 1 big = math.ldexp(big, 127 - 23) - packed = struct.pack(format, big) - unpacked = struct.unpack(format, packed)[0] + packed = struct.pack(">f", big) + unpacked = struct.unpack(">f", packed)[0] self.assertEqual(big, unpacked) # The same, but tack on a 1 bit so it rounds up to infinity. big = (1 << 25) - 1 big = math.ldexp(big, 127 - 24) - self.assertRaises(OverflowError, struct.pack, format, big) - - def test_705836(self): - self.check_705836("f") - self.check_705836(">f", "f") + self.assertRaises(OverflowError, struct.pack, ">f", big) + self.assertRaises(OverflowError, struct.pack, "e", big) + unpacked = struct.unpack(">e", packed)[0] + self.assertEqual(big, unpacked) + big = (1 << 12) - 1 + big = math.ldexp(big, 15 - 11) + self.assertRaises(OverflowError, struct.pack, ">e", big) + self.assertRaises(OverflowError, struct.pack, "f", "d", "d"): - with self.subTest(format=format): - f = struct.unpack(format, struct.pack(format, 1.5))[0] - self.assertEqual(f, 1.5) - f = struct.unpack(format, struct.pack(format, NAN))[0] - self.assertTrue(math.isnan(f), f) - f = struct.unpack(format, struct.pack(format, INF))[0] - self.assertTrue(math.isinf(f), f) - self.assertEqual(math.copysign(1.0, f), 1.0) - f = struct.unpack(format, struct.pack(format, -INF))[0] - self.assertTrue(math.isinf(f), f) - self.assertEqual(math.copysign(1.0, f), -1.0) - if __name__ == '__main__': unittest.main() From 7eaf9a9edda750feee01fcf518f3bfb1fe6c598d Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 26 Mar 2026 11:02:40 +0100 Subject: [PATCH 3/4] Add test_float_round_trip() --- Lib/test/test_struct.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index 2942ae66f2b0e1..6479676f155eca 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -1031,6 +1031,24 @@ def test_operations_on_half_initialized_Struct(self): self.assertRaises(RuntimeError, repr, S) self.assertEqual(S.size, -1) + def test_float_round_trip(self): + for format in ( + "f", "f", + "d", "d", + "e", "e", + ): + with self.subTest(format=format): + f = struct.unpack(format, struct.pack(format, 1.5))[0] + self.assertEqual(f, 1.5) + f = struct.unpack(format, struct.pack(format, NAN))[0] + self.assertTrue(math.isnan(f), f) + f = struct.unpack(format, struct.pack(format, INF))[0] + self.assertTrue(math.isinf(f), f) + self.assertEqual(math.copysign(1.0, f), 1.0) + f = struct.unpack(format, struct.pack(format, -INF))[0] + self.assertTrue(math.isinf(f), f) + self.assertEqual(math.copysign(1.0, f), -1.0) + class UnpackIteratorTest(unittest.TestCase): """ From d4e1edf495d4384f5b373c9a40b67fa40ae868fc Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 26 Mar 2026 11:04:53 +0100 Subject: [PATCH 4/4] Add NEWS entry --- .../next/Library/2026-03-26-11-04-42.gh-issue-145633.RWjlaX.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-03-26-11-04-42.gh-issue-145633.RWjlaX.rst diff --git a/Misc/NEWS.d/next/Library/2026-03-26-11-04-42.gh-issue-145633.RWjlaX.rst b/Misc/NEWS.d/next/Library/2026-03-26-11-04-42.gh-issue-145633.RWjlaX.rst new file mode 100644 index 00000000000000..00507fe89d07ec --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-26-11-04-42.gh-issue-145633.RWjlaX.rst @@ -0,0 +1,2 @@ +Fix ``struct.pack('f', float)``: use :c:func:`PyFloat_Pack4` to raise +:exc:`OverflowError`. Patch by Sergey B Kirpichev and Victor Stinner.