Fix: foreach_get/set does not work on multidimensional arrays

The foreach_get/foreach_set methods of bpy_prop_array get/set the entire
contents of the array, but they were checking that the length of the
input sequence was equal to the length of the current array dimension
rather than the total length of all dimensions of the array.

This would read/write memory after the end of the passed in sequence
when the property was a multidimensional array. Performing
`foreach_get` with a Python list with length matching the length of the
current dimension of a multidimensional array would crash a debug build
due to the trailing pad bytes of the temporarily allocated array being
overwritten.

This patch fixes `pyprop_array_foreach_getset` by changing the function
used to get the expected sequence size, to the RNA function that gets
the total length of the array across all its dimensions.

The tests have be updated to additionally test multidimensional array
properties.

Pull Request: https://projects.blender.org/blender/blender/pulls/116457
This commit is contained in:
Thomas Barlow
2024-01-12 18:38:32 +01:00
committed by Brecht Van Lommel
parent 9c7b3659e2
commit 5139a9c064
2 changed files with 110 additions and 60 deletions

View File

@@ -50,78 +50,128 @@ def seq_items_as_dims(data):
class TestPropArray(unittest.TestCase):
def setUp(self):
id_type.test_array_f = FloatVectorProperty(size=10)
id_type.test_array_f_2d = FloatVectorProperty(size=(4, 1))
id_type.test_array_f_3d = FloatVectorProperty(size=(3, 2, 4))
id_type.test_array_i = IntVectorProperty(size=10)
scene = bpy.context.scene
self.array_f = scene.test_array_f
self.array_i = scene.test_array_i
id_type.test_array_i_2d = IntVectorProperty(size=(4, 1))
id_type.test_array_i_3d = IntVectorProperty(size=(3, 2, 4))
def tearDown(self):
del id_type.test_array_f
del id_type.test_array_f_2d
del id_type.test_array_f_3d
del id_type.test_array_i
del id_type.test_array_i_2d
del id_type.test_array_i_3d
@staticmethod
def parse_test_args(prop_array_first_dim, prop_type, prop_size):
match prop_type:
case 'INT':
expected_dtype = np.int32
wrong_kind_dtype = np.float32
wrong_size_dtype = np.int64
case 'FLOAT':
expected_dtype = np.float32
wrong_kind_dtype = np.int32
wrong_size_dtype = np.float64
case _:
raise AssertionError("Unexpected property type '%s'" % prop_type)
expected_length = np.prod(prop_size)
num_dims = len(prop_size)
assert expected_length > 0
too_short_length = expected_length - 1
match num_dims:
case 1:
def get_flat_iterable_all_dimensions():
return prop_array_first_dim[:]
case 2:
def get_flat_iterable_all_dimensions():
return (flat_elem for array_1d in prop_array_first_dim[:] for flat_elem in array_1d[:])
case 3:
def get_flat_iterable_all_dimensions():
return (flat_elem
for array_2d in prop_array_first_dim[:]
for array_1d in array_2d[:]
for flat_elem in array_1d[:])
case _:
raise AssertionError("Number of dimensions must be 1, 2 or 3, but was %i" % num_dims)
return (expected_dtype, wrong_kind_dtype, wrong_size_dtype, expected_length, too_short_length,
get_flat_iterable_all_dimensions)
def do_test_foreach_getset_current_dimension(self, prop_array, expected_dtype, wrong_kind_dtype, wrong_size_dtype,
expected_length, too_short_length, get_flat_iterable_all_dimensions):
with self.assertRaises(TypeError):
prop_array.foreach_set(range(too_short_length))
prop_array.foreach_set(range(5, 5 + expected_length))
with self.assertRaises(TypeError):
prop_array.foreach_set(np.arange(too_short_length, dtype=expected_dtype))
with self.assertRaises(TypeError):
prop_array.foreach_set(np.arange(expected_length, dtype=wrong_size_dtype))
with self.assertRaises(TypeError):
prop_array.foreach_get(np.arange(expected_length, dtype=wrong_kind_dtype))
a = np.arange(expected_length, dtype=expected_dtype)
prop_array.foreach_set(a)
with self.assertRaises(TypeError):
prop_array.foreach_set(a[:too_short_length])
for v1, v2 in zip(a, get_flat_iterable_all_dimensions()):
self.assertEqual(v1, v2)
b = np.empty(expected_length, dtype=expected_dtype)
prop_array.foreach_get(b)
for v1, v2 in zip(a, b):
self.assertEqual(v1, v2)
b = [None] * expected_length
prop_array.foreach_get(b)
for v1, v2 in zip(a, b):
self.assertEqual(v1, v2)
def do_test_foreach_getset(self, prop_array, prop_type, prop_size):
if not isinstance(prop_size, (tuple, list)):
prop_size = (prop_size,)
num_dimensions = len(prop_size)
test_args = self.parse_test_args(prop_array, prop_type, prop_size)
# Test that foreach_get/foreach_set work, and work the same regardless of the current dimension/sub-array being
# accessed.
self.do_test_foreach_getset_current_dimension(prop_array, *test_args)
if num_dimensions > 1:
for i in range(prop_size[0]):
self.do_test_foreach_getset_current_dimension(prop_array[i], *test_args)
if num_dimensions > 2:
for j in range(prop_size[1]):
self.do_test_foreach_getset_current_dimension(prop_array[i][j], *test_args)
def test_foreach_getset_i(self):
with self.assertRaises(TypeError):
self.array_i.foreach_set(range(5))
self.array_i.foreach_set(range(5, 15))
with self.assertRaises(TypeError):
self.array_i.foreach_set(np.arange(5, dtype=np.int32))
with self.assertRaises(TypeError):
self.array_i.foreach_set(np.arange(10, dtype=np.int64))
with self.assertRaises(TypeError):
self.array_i.foreach_get(np.arange(10, dtype=np.float32))
a = np.arange(10, dtype=np.int32)
self.array_i.foreach_set(a)
with self.assertRaises(TypeError):
self.array_i.foreach_set(a[:5])
for v1, v2 in zip(a, self.array_i[:]):
self.assertEqual(v1, v2)
b = np.empty(10, dtype=np.int32)
self.array_i.foreach_get(b)
for v1, v2 in zip(a, b):
self.assertEqual(v1, v2)
b = [None] * 10
self.array_f.foreach_get(b)
for v1, v2 in zip(a, b):
self.assertEqual(v1, v2)
self.do_test_foreach_getset(id_inst.test_array_i, 'INT', 10)
def test_foreach_getset_f(self):
with self.assertRaises(TypeError):
self.array_i.foreach_set(range(5))
self.do_test_foreach_getset(id_inst.test_array_f, 'FLOAT', 10)
self.array_f.foreach_set(range(5, 15))
def test_foreach_getset_i_2d(self):
self.do_test_foreach_getset(id_inst.test_array_i_2d, 'INT', (4, 1))
with self.assertRaises(TypeError):
self.array_f.foreach_set(np.arange(5, dtype=np.float32))
def test_foreach_getset_f_2d(self):
self.do_test_foreach_getset(id_inst.test_array_f_2d, 'FLOAT', (4, 1))
with self.assertRaises(TypeError):
self.array_f.foreach_set(np.arange(10, dtype=np.int32))
def test_foreach_getset_i_3d(self):
self.do_test_foreach_getset(id_inst.test_array_i_3d, 'INT', (3, 2, 4))
with self.assertRaises(TypeError):
self.array_f.foreach_get(np.arange(10, dtype=np.float64))
a = np.arange(10, dtype=np.float32)
self.array_f.foreach_set(a)
for v1, v2 in zip(a, self.array_f[:]):
self.assertEqual(v1, v2)
b = np.empty(10, dtype=np.float32)
self.array_f.foreach_get(b)
for v1, v2 in zip(a, b):
self.assertEqual(v1, v2)
b = [None] * 10
self.array_f.foreach_get(b)
for v1, v2 in zip(a, b):
self.assertEqual(v1, v2)
def test_foreach_getset_f_3d(self):
self.do_test_foreach_getset(id_inst.test_array_f_3d, 'FLOAT', (3, 2, 4))
class TestPropArrayMultiDimensional(unittest.TestCase):