Skip to content

Commit 4a42156

Browse files
authored
test_eigen.py test_nonunit_stride_to_python bug fix (ASAN failure) (#4217)
* Disable test triggering ASAN failure (to pin-point where the problem is). * Fix unsafe "block" implementation in test_eigen.cpp * Undo changes (i.e. revert back to master). * Detect "type_caster for Eigen::Ref made a copy." This is achieved without * reaching into internals, * making test_eigen.cpp depend on pybind11/numpy.h. * Add comment pointing to PR, for easy reference.
1 parent 6cb2147 commit 4a42156

File tree

2 files changed

+46
-8
lines changed

2 files changed

+46
-8
lines changed

tests/test_eigen.cpp

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,40 @@ TEST_SUBMODULE(eigen, m) {
197197

198198
// Return a block of a matrix (gives non-standard strides)
199199
m.def("block",
200-
[](const Eigen::Ref<const Eigen::MatrixXd> &x,
201-
int start_row,
202-
int start_col,
203-
int block_rows,
204-
int block_cols) { return x.block(start_row, start_col, block_rows, block_cols); });
200+
[m](const py::object &x_obj,
201+
int start_row,
202+
int start_col,
203+
int block_rows,
204+
int block_cols) {
205+
return m.attr("_block")(x_obj, x_obj, start_row, start_col, block_rows, block_cols);
206+
});
207+
208+
m.def(
209+
"_block",
210+
[](const py::object &x_obj,
211+
const Eigen::Ref<const Eigen::MatrixXd> &x,
212+
int start_row,
213+
int start_col,
214+
int block_rows,
215+
int block_cols) {
216+
// See PR #4217 for background. This test is a bit over the top, but might be useful
217+
// as a concrete example to point to when explaining the dangling reference trap.
218+
auto i0 = py::make_tuple(0, 0);
219+
auto x0_orig = x_obj[*i0].cast<double>();
220+
if (x(0, 0) != x0_orig) {
221+
throw std::runtime_error(
222+
"Something in the type_caster for Eigen::Ref is terribly wrong.");
223+
}
224+
double x0_mod = x0_orig + 1;
225+
x_obj[*i0] = x0_mod;
226+
auto copy_detected = (x(0, 0) != x0_mod);
227+
x_obj[*i0] = x0_orig;
228+
if (copy_detected) {
229+
throw std::runtime_error("type_caster for Eigen::Ref made a copy.");
230+
}
231+
return x.block(start_row, start_col, block_rows, block_cols);
232+
},
233+
py::keep_alive<0, 1>());
205234

206235
// test_eigen_return_references, test_eigen_keepalive
207236
// return value referencing/copying tests:

tests/test_eigen.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,15 +217,24 @@ def test_negative_stride_from_python(msg):
217217
)
218218

219219

220+
def test_block_runtime_error_type_caster_eigen_ref_made_a_copy():
221+
with pytest.raises(RuntimeError) as excinfo:
222+
m.block(ref, 0, 0, 0, 0)
223+
assert str(excinfo.value) == "type_caster for Eigen::Ref made a copy."
224+
225+
220226
def test_nonunit_stride_to_python():
221227
assert np.all(m.diagonal(ref) == ref.diagonal())
222228
assert np.all(m.diagonal_1(ref) == ref.diagonal(1))
223229
for i in range(-5, 7):
224230
assert np.all(m.diagonal_n(ref, i) == ref.diagonal(i)), f"m.diagonal_n({i})"
225231

226-
assert np.all(m.block(ref, 2, 1, 3, 3) == ref[2:5, 1:4])
227-
assert np.all(m.block(ref, 1, 4, 4, 2) == ref[1:, 4:])
228-
assert np.all(m.block(ref, 1, 4, 3, 2) == ref[1:4, 4:])
232+
# Must be order="F", otherwise the type_caster will make a copy and
233+
# m.block() will return a dangling reference (heap-use-after-free).
234+
rof = np.asarray(ref, order="F")
235+
assert np.all(m.block(rof, 2, 1, 3, 3) == rof[2:5, 1:4])
236+
assert np.all(m.block(rof, 1, 4, 4, 2) == rof[1:, 4:])
237+
assert np.all(m.block(rof, 1, 4, 3, 2) == rof[1:4, 4:])
229238

230239

231240
def test_eigen_ref_to_python():

0 commit comments

Comments
 (0)