diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 656318668e6d6e..7b04a4ddd83735 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -575,6 +575,28 @@ def test_array_assign(self): m[:] = new_a self.assertEqual(a, new_a) + 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) + + allbytes = bytes(range(256)) + allbytes = memoryview(allbytes).cast('?') + self.assertEqual(allbytes.tolist(), [False] + [True] * 255) + class BytesMemorySliceTest(unittest.TestCase, BaseMemorySliceTests, BaseBytesMemoryTests): 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..881964673307cc --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-12-17-27-28.gh-issue-148390.MAhw7F.rst @@ -0,0 +1,5 @@ +Fix an undefined behavior in :class:`memoryview` when using the native +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. diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 1bc03c254f05d0..032060163c9429 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -1672,6 +1672,10 @@ fix_error_int(const char *fmt) return -1; } +// UNPACK_TO_BOOL: Return 0 if PTR represents "false", and 1 otherwise. +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 pylong_as_ld(PyObject *item) @@ -1807,7 +1811,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; @@ -2989,7 +2993,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; diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index 3c3cb2f9c86f16..515577516f759e 100644 --- a/Tools/c-analyzer/cpython/globals-to-fix.tsv +++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv @@ -354,7 +354,7 @@ Modules/_testclinic.c - TestClass - ################################## ## global non-objects to fix in builtin modules -# +Objects/memoryobject.c - bool_false - ##################################