Skip to content

Commit b831044

Browse files
picnixzStanFromIreland
authored andcommitted
[3.14] pythongh-148390: fix undefined behavior of memoryview(...).cast("?") (pythonGH-148454)
(cherry picked from commit 69e0a78) Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
1 parent 2fdccb3 commit b831044

File tree

4 files changed

+107
-3
lines changed

4 files changed

+107
-3
lines changed

Lib/test/test_memoryview.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,101 @@ def test_array_assign(self):
575575
m[:] = new_a
576576
self.assertEqual(a, new_a)
577577

578+
def test_compare_equal(self):
579+
# A memoryview is equal to itself: there is no need to compare
580+
# individual values. This is not true for float values since they can
581+
# be NaN, and NaN is not equal to itself.
582+
583+
def check_equal(view, is_equal):
584+
self.assertEqual(view == view, is_equal)
585+
self.assertEqual(view != view, not is_equal)
586+
587+
# Comparison with a different memoryview doesn't use
588+
# the optimization and should give the same result.
589+
view2 = memoryview(view)
590+
self.assertEqual(view2 == view, is_equal)
591+
self.assertEqual(view2 != view2, not is_equal)
592+
593+
# Test integer formats
594+
for int_format in 'bBhHiIlLqQ':
595+
with self.subTest(format=int_format):
596+
a = array.array(int_format, [1, 2, 3])
597+
m = memoryview(a)
598+
check_equal(m, True)
599+
600+
if int_format in 'bB':
601+
m2 = m.cast('@' + m.format)
602+
check_equal(m2, True)
603+
604+
# Test 'c' format
605+
a = array.array('B', [1, 2, 3])
606+
m = memoryview(a.tobytes()).cast('c')
607+
check_equal(m, True)
608+
609+
# Test 'n' and 'N' formats
610+
if struct.calcsize('L') == struct.calcsize('N'):
611+
int_format = 'L'
612+
elif struct.calcsize('Q') == struct.calcsize('N'):
613+
int_format = 'Q'
614+
else:
615+
int_format = None
616+
if int_format:
617+
a = array.array(int_format, [1, 2, 3])
618+
m = memoryview(a.tobytes()).cast('N')
619+
check_equal(m, True)
620+
m = memoryview(a.tobytes()).cast('n')
621+
check_equal(m, True)
622+
623+
# Test '?' format
624+
m = memoryview(b'\0\1\2').cast('?')
625+
check_equal(m, True)
626+
627+
# Test float formats
628+
for float_format in 'fd':
629+
with self.subTest(format=float_format):
630+
a = array.array(float_format, [1.0, 2.0, float('nan')])
631+
m = memoryview(a)
632+
# nan is not equal to nan
633+
check_equal(m, False)
634+
635+
a = array.array(float_format, [1.0, 2.0, 3.0])
636+
m = memoryview(a)
637+
check_equal(m, True)
638+
639+
# Test complex formats
640+
for complex_format in 'FD':
641+
with self.subTest(format=complex_format):
642+
data = struct.pack(complex_format * 3, 1.0, 2.0, float('nan'))
643+
m = memoryview(data).cast(complex_format)
644+
# nan is not equal to nan
645+
check_equal(m, False)
646+
647+
data = struct.pack(complex_format * 3, 1.0, 2.0, 3.0)
648+
m = memoryview(data).cast(complex_format)
649+
check_equal(m, True)
650+
651+
def test_boolean_format(self):
652+
# Test '?' format (keep all the checks below for UBSan)
653+
# See github.com/python/cpython/issues/148390.
654+
655+
# m1a and m1b are equivalent to [False, True, False]
656+
m1a = memoryview(b'\0\2\0').cast('?')
657+
self.assertEqual(m1a.tolist(), [False, True, False])
658+
m1b = memoryview(b'\0\4\0').cast('?')
659+
self.assertEqual(m1b.tolist(), [False, True, False])
660+
self.assertEqual(m1a, m1b)
661+
662+
# m2a and m2b are equivalent to [True, True, True]
663+
m2a = memoryview(b'\1\3\5').cast('?')
664+
self.assertEqual(m2a.tolist(), [True, True, True])
665+
m2b = memoryview(b'\2\4\6').cast('?')
666+
self.assertEqual(m2b.tolist(), [True, True, True])
667+
self.assertEqual(m2a, m2b)
668+
669+
allbytes = bytes(range(256))
670+
allbytes = memoryview(allbytes).cast('?')
671+
self.assertEqual(allbytes.tolist(), [False] + [True] * 255)
672+
578673

579674
class BytesMemorySliceTest(unittest.TestCase,
580675
BaseMemorySliceTests, BaseBytesMemoryTests):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix an undefined behavior in :class:`memoryview` when using the native
2+
boolean format (``?``) in :meth:`~memoryview.cast`. Previously, on some
3+
common platforms, calling ``memoryview(b).cast("?").tolist()`` incorrectly
4+
returned ``[False]`` instead of ``[True]`` for any even byte *b*.
5+
Patch by Bénédikt Tran.

Objects/memoryobject.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,6 +1672,10 @@ fix_error_int(const char *fmt)
16721672
return -1;
16731673
}
16741674

1675+
// UNPACK_TO_BOOL: Return 0 if PTR represents "false", and 1 otherwise.
1676+
static const _Bool bool_false = 0;
1677+
#define UNPACK_TO_BOOL(PTR) (memcmp((PTR), &bool_false, sizeof(_Bool)) != 0)
1678+
16751679
/* Accept integer objects or objects with an __index__() method. */
16761680
static long
16771681
pylong_as_ld(PyObject *item)
@@ -1807,7 +1811,7 @@ unpack_single(PyMemoryViewObject *self, const char *ptr, const char *fmt)
18071811
case 'l': UNPACK_SINGLE(ld, ptr, long); goto convert_ld;
18081812

18091813
/* boolean */
1810-
case '?': UNPACK_SINGLE(ld, ptr, _Bool); goto convert_bool;
1814+
case '?': ld = UNPACK_TO_BOOL(ptr); goto convert_bool;
18111815

18121816
/* unsigned integers */
18131817
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,
29892993
case 'l': CMP_SINGLE(p, q, long); return equal;
29902994

29912995
/* boolean */
2992-
case '?': CMP_SINGLE(p, q, _Bool); return equal;
2996+
case '?': return UNPACK_TO_BOOL(p) == UNPACK_TO_BOOL(q);
29932997

29942998
/* unsigned integers */
29952999
case 'H': CMP_SINGLE(p, q, unsigned short); return equal;

Tools/c-analyzer/cpython/globals-to-fix.tsv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ Modules/_testclinic.c - TestClass -
354354
##################################
355355
## global non-objects to fix in builtin modules
356356

357-
# <none>
357+
Objects/memoryobject.c - bool_false -
358358

359359

360360
##################################

0 commit comments

Comments
 (0)