From 8d117b766ab211ff4d5efb9d25806c86617ca69a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:27:59 +0200 Subject: [PATCH 1/5] gh-148390: fix undefined behavior of `memoryview(...).cast("?")` --- Lib/test/test_memoryview.py | 14 +++++++++++++- .../2026-04-12-17-27-28.gh-issue-148390.MAhw7F.rst | 4 ++++ Objects/memoryobject.c | 7 +++++-- 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-04-12-17-27-28.gh-issue-148390.MAhw7F.rst diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 22b9f6af758f88..3bcede6b3b73f0 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -620,9 +620,21 @@ def check_equal(view, is_equal): m = memoryview(a.tobytes()).cast('n') check_equal(m, True) - # Test '?' format + # Test '?' format (keep all the checks below for UBSan) m = memoryview(b'\0\1\2').cast('?') check_equal(m, True) + # m1a and m1b are equivalent to [False, True, False] + m1a = memoryview(b'\0\2\0').cast('?') + self.assertEqual(m1a.tolist(), [False, True, False]) + m1b = memoryview(b'\0\4\0').cast('?') + self.assertEqual(m1b.tolist(), [False, True, False]) + self.assertEqual(m1a, m1b) + # m1a and m1b are equivalent to [True, True, True] + m2a = memoryview(b'\1\3\5').cast('?') + self.assertEqual(m2a.tolist(), [True, True, True]) + m2b = memoryview(b'\2\4\6').cast('?') + self.assertEqual(m2b.tolist(), [True, True, True]) + self.assertEqual(m2a, m2b) # Test float formats for float_format in 'fd': diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-12-17-27-28.gh-issue-148390.MAhw7F.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-12-17-27-28.gh-issue-148390.MAhw7F.rst new file mode 100644 index 00000000000000..44df2ed0bb102a --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-12-17-27-28.gh-issue-148390.MAhw7F.rst @@ -0,0 +1,4 @@ +Fix an undefined behavior in :class:`memoryview` when using the native +boolean format (``?``) in :meth:`~memoryview.cast`. Previously, calling +``memoryview(b).cast("?").tolist()`` incorrectly returned ``[False]`` +instead of ``[True]`` for any even byte *b*. Patch by Bénédikt Tran. diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 4cbbb7eb7cd0fd..42fc89872ab77f 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -1676,6 +1676,9 @@ fix_error_int(const char *fmt) return -1; } +// Return 0 if PTR represents "false", and 1 otherwise. +#define UNPACK_TO_BOOL(PTR) (memcmp((PTR), &(_Bool){0}, sizeof(_Bool)) != 0) + /* Accept integer objects or objects with an __index__() method. */ static long pylong_as_ld(PyObject *item) @@ -1811,7 +1814,7 @@ unpack_single(PyMemoryViewObject *self, const char *ptr, const char *fmt) case 'l': UNPACK_SINGLE(ld, ptr, long); goto convert_ld; /* boolean */ - case '?': UNPACK_SINGLE(ld, ptr, _Bool); goto convert_bool; + case '?': ld = UNPACK_TO_BOOL(ptr); goto convert_bool; /* unsigned integers */ case 'H': UNPACK_SINGLE(lu, ptr, unsigned short); goto convert_lu; @@ -3029,7 +3032,7 @@ unpack_cmp(const char *p, const char *q, char fmt, case 'l': CMP_SINGLE(p, q, long); return equal; /* boolean */ - case '?': CMP_SINGLE(p, q, _Bool); return equal; + case '?': return UNPACK_TO_BOOL(p) == UNPACK_TO_BOOL(q); /* unsigned integers */ case 'H': CMP_SINGLE(p, q, unsigned short); return equal; From bc34ab9a0e78778d3663039a957565bfa060ddf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:51:24 +0200 Subject: [PATCH 2/5] update file suppression --- Tools/ubsan/suppressions.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tools/ubsan/suppressions.txt b/Tools/ubsan/suppressions.txt index 9a5f20364261fe..a00e256b333618 100644 --- a/Tools/ubsan/suppressions.txt +++ b/Tools/ubsan/suppressions.txt @@ -9,9 +9,6 @@ # Objects/object.c:97:5: runtime error: member access within null pointer of type 'PyThreadState' (aka 'struct _ts') null:Objects/object.c -# Objects/memoryobject.c:3032:15: runtime error: load of value 2, which is not a valid value for type 'bool' -bool:Objects/memoryobject.c - # Modules/_ctypes/cfield.c:644:1: runtime error: left shift of 1 by 63 places cannot be represented in type 'int64_t' (aka 'long') shift-base:Modules/_ctypes/cfield.c From 735d8ae090c69241b164a3fa7dce6bffaf1c50f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:01:17 +0200 Subject: [PATCH 3/5] use `static const` instead of C99 compound literals --- Objects/memoryobject.c | 3 ++- Tools/c-analyzer/cpython/globals-to-fix.tsv | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 42fc89872ab77f..540f064632698b 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -1677,7 +1677,8 @@ fix_error_int(const char *fmt) } // Return 0 if PTR represents "false", and 1 otherwise. -#define UNPACK_TO_BOOL(PTR) (memcmp((PTR), &(_Bool){0}, sizeof(_Bool)) != 0) +static const _Bool bool_false = 0; +#define UNPACK_TO_BOOL(PTR) (memcmp((PTR), &bool_false, sizeof(_Bool)) != 0) /* Accept integer objects or objects with an __index__() method. */ static long diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index d645d2b6150d34..74ca562824012b 100644 --- a/Tools/c-analyzer/cpython/globals-to-fix.tsv +++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv @@ -357,7 +357,7 @@ Modules/_testclinic.c - TestClass - ################################## ## global non-objects to fix in builtin modules -# +Objects/memoryobject.c - bool_false - ################################## From 9b1982291f9b1453a8a4c5339a00e4c94abd6477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:01:40 +0200 Subject: [PATCH 4/5] move test into its own test function --- Lib/test/test_memoryview.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 3bcede6b3b73f0..8d3a59ff67cb58 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -620,21 +620,9 @@ def check_equal(view, is_equal): m = memoryview(a.tobytes()).cast('n') check_equal(m, True) - # Test '?' format (keep all the checks below for UBSan) + # Test '?' format m = memoryview(b'\0\1\2').cast('?') check_equal(m, True) - # m1a and m1b are equivalent to [False, True, False] - m1a = memoryview(b'\0\2\0').cast('?') - self.assertEqual(m1a.tolist(), [False, True, False]) - m1b = memoryview(b'\0\4\0').cast('?') - self.assertEqual(m1b.tolist(), [False, True, False]) - self.assertEqual(m1a, m1b) - # m1a and m1b are equivalent to [True, True, True] - m2a = memoryview(b'\1\3\5').cast('?') - self.assertEqual(m2a.tolist(), [True, True, True]) - m2b = memoryview(b'\2\4\6').cast('?') - self.assertEqual(m2b.tolist(), [True, True, True]) - self.assertEqual(m2a, m2b) # Test float formats for float_format in 'fd': @@ -660,6 +648,24 @@ def check_equal(view, is_equal): m = memoryview(data).cast(complex_format) check_equal(m, True) + def test_boolean_format(self): + # Test '?' format (keep all the checks below for UBSan) + # See github.com/python/cpython/issues/148390. + + # m1a and m1b are equivalent to [False, True, False] + m1a = memoryview(b'\0\2\0').cast('?') + self.assertEqual(m1a.tolist(), [False, True, False]) + m1b = memoryview(b'\0\4\0').cast('?') + self.assertEqual(m1b.tolist(), [False, True, False]) + self.assertEqual(m1a, m1b) + + # m2a and m2b are equivalent to [True, True, True] + m2a = memoryview(b'\1\3\5').cast('?') + self.assertEqual(m2a.tolist(), [True, True, True]) + m2b = memoryview(b'\2\4\6').cast('?') + self.assertEqual(m2b.tolist(), [True, True, True]) + self.assertEqual(m2a, m2b) + class BytesMemorySliceTest(unittest.TestCase, BaseMemorySliceTests, BaseBytesMemoryTests): From 8298d67d9b176132bf312ad76549097a147cea28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:01:43 +0200 Subject: [PATCH 5/5] update NEWS entry --- .../2026-04-12-17-27-28.gh-issue-148390.MAhw7F.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-12-17-27-28.gh-issue-148390.MAhw7F.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-12-17-27-28.gh-issue-148390.MAhw7F.rst index 44df2ed0bb102a..881964673307cc 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-12-17-27-28.gh-issue-148390.MAhw7F.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-12-17-27-28.gh-issue-148390.MAhw7F.rst @@ -1,4 +1,5 @@ Fix an undefined behavior in :class:`memoryview` when using the native -boolean format (``?``) in :meth:`~memoryview.cast`. Previously, calling -``memoryview(b).cast("?").tolist()`` incorrectly returned ``[False]`` -instead of ``[True]`` for any even byte *b*. Patch by Bénédikt Tran. +boolean format (``?``) in :meth:`~memoryview.cast`. Previously, on some +common platforms, calling ``memoryview(b).cast("?").tolist()`` incorrectly +returned ``[False]`` instead of ``[True]`` for any even byte *b*. +Patch by Bénédikt Tran.