Skip to content

Commit 465d4fb

Browse files
committed
Add tests for accessing uninitialized holders
1 parent 9a191c2 commit 465d4fb

File tree

4 files changed

+179
-0
lines changed

4 files changed

+179
-0
lines changed

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ set(PYBIND11_TEST_FILES
148148
test_exceptions
149149
test_factory_constructors
150150
test_gil_scoped
151+
test_invalid_holder_access
151152
test_iostream
152153
test_kwargs_and_defaults
153154
test_local_bindings

tests/pybind11_tests.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@ const char *cpp_std() {
7575
#endif
7676
}
7777

78+
int cpp_std_num() {
79+
return
80+
#if defined(PYBIND11_CPP20)
81+
20;
82+
#elif defined(PYBIND11_CPP17)
83+
17;
84+
#elif defined(PYBIND11_CPP14)
85+
14;
86+
#else
87+
11;
88+
#endif
89+
}
90+
7891
PYBIND11_MODULE(pybind11_tests, m, py::mod_gil_not_used()) {
7992
m.doc() = "pybind11 test module";
8093

@@ -88,6 +101,7 @@ PYBIND11_MODULE(pybind11_tests, m, py::mod_gil_not_used()) {
88101
m.attr("compiler_info") = py::none();
89102
#endif
90103
m.attr("cpp_std") = cpp_std();
104+
m.attr("cpp_std_num") = cpp_std_num();
91105
m.attr("PYBIND11_INTERNALS_ID") = PYBIND11_INTERNALS_ID;
92106
// Free threaded Python uses UINT32_MAX for immortal objects.
93107
m.attr("PYBIND11_REFCNT_IMMORTAL") = UINT32_MAX;

tests/test_invalid_holder_access.cpp

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#include "pybind11_tests.h"
2+
3+
#include <Python.h>
4+
#include <memory>
5+
#include <vector>
6+
7+
class VectorOwns4PythonObjects {
8+
public:
9+
void append(const py::object &obj) {
10+
if (size() >= 4) {
11+
throw std::out_of_range("Index out of range");
12+
}
13+
vec.emplace_back(obj);
14+
}
15+
16+
void set_item(py::ssize_t i, const py::object &obj) {
17+
if (!(i >= 0 && i < size())) {
18+
throw std::out_of_range("Index out of range");
19+
}
20+
vec[py::size_t(i)] = obj;
21+
}
22+
23+
py::object get_item(py::ssize_t i) const {
24+
if (!(i >= 0 && i < size())) {
25+
throw std::out_of_range("Index out of range");
26+
}
27+
return vec[py::size_t(i)];
28+
}
29+
30+
py::ssize_t size() const { return py::ssize_t_cast(vec.size()); }
31+
32+
bool is_empty() const { return vec.empty(); }
33+
34+
void sanity_check() const {
35+
auto current_size = size();
36+
if (current_size < 0 || current_size > 4) {
37+
throw std::out_of_range("Invalid size");
38+
}
39+
}
40+
41+
static int tp_traverse(PyObject *self_base, visitproc visit, void *arg) {
42+
#if PY_VERSION_HEX >= 0x03090000 // Python 3.9
43+
Py_VISIT(Py_TYPE(self_base));
44+
#endif
45+
auto *const instance = reinterpret_cast<py::detail::instance *>(self_base);
46+
if (!instance->get_value_and_holder().holder_constructed()) {
47+
// The holder has not been constructed yet. Skip the traversal to avoid segmentation
48+
// faults.
49+
return 0;
50+
}
51+
auto &self = py::cast<VectorOwns4PythonObjects &>(py::handle{self_base});
52+
for (const auto &obj : self.vec) {
53+
Py_VISIT(obj.ptr());
54+
}
55+
return 0;
56+
}
57+
58+
private:
59+
std::vector<py::object> vec{};
60+
};
61+
62+
TEST_SUBMODULE(invalid_holder_access, m) {
63+
m.doc() = "Test invalid holder access";
64+
65+
#if defined(PYBIND11_CPP14)
66+
m.def("create_vector", []() -> std::unique_ptr<VectorOwns4PythonObjects> {
67+
auto vec = std::make_unique<VectorOwns4PythonObjects>();
68+
vec->append(py::none());
69+
vec->append(py::int_(1));
70+
vec->append(py::str("test"));
71+
vec->append(py::tuple());
72+
return vec;
73+
});
74+
#endif
75+
76+
py::class_<VectorOwns4PythonObjects>(
77+
m,
78+
"VectorOwns4PythonObjects",
79+
py::custom_type_setup([](PyHeapTypeObject *heap_type) -> void {
80+
auto *const type = &heap_type->ht_type;
81+
type->tp_flags |= Py_TPFLAGS_HAVE_GC;
82+
type->tp_traverse = &VectorOwns4PythonObjects::tp_traverse;
83+
}))
84+
.def("append", &VectorOwns4PythonObjects::append, py::arg("obj"))
85+
.def("set_item", &VectorOwns4PythonObjects::set_item, py::arg("i"), py::arg("obj"))
86+
.def("get_item", &VectorOwns4PythonObjects::get_item, py::arg("i"))
87+
.def("size", &VectorOwns4PythonObjects::size)
88+
.def("is_empty", &VectorOwns4PythonObjects::is_empty)
89+
.def("__setitem__", &VectorOwns4PythonObjects::set_item, py::arg("i"), py::arg("obj"))
90+
.def("__getitem__", &VectorOwns4PythonObjects::get_item, py::arg("i"))
91+
.def("__len__", &VectorOwns4PythonObjects::size)
92+
.def("sanity_check", &VectorOwns4PythonObjects::sanity_check);
93+
}

tests/test_invalid_holder_access.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from __future__ import annotations
2+
3+
import gc
4+
import weakref
5+
6+
import pytest
7+
8+
import pybind11_tests
9+
from pybind11_tests import invalid_holder_access as m
10+
11+
XFAIL_REASON = "Known issues: https://github.com/pybind/pybind11/pull/5654"
12+
13+
14+
@pytest.mark.skipif(
15+
pybind11_tests.cpp_std_num < 14,
16+
reason="std::{unique_ptr,make_unique} not available in C++11",
17+
)
18+
def test_create_vector():
19+
vec = m.create_vector()
20+
assert vec.size() == 4
21+
assert not vec.is_empty()
22+
assert vec[0] is None
23+
assert vec[1] == 1
24+
assert vec[2] == "test"
25+
assert vec[3] == ()
26+
27+
28+
def test_no_init():
29+
with pytest.raises(TypeError, match=r"No constructor defined"):
30+
m.VectorOwns4PythonObjects()
31+
vec = m.VectorOwns4PythonObjects.__new__(m.VectorOwns4PythonObjects)
32+
with pytest.raises(TypeError, match=r"No constructor defined"):
33+
vec.__init__()
34+
35+
36+
# The test might succeed on some platforms with some compilers, but
37+
# it is not guaranteed to work everywhere. It is marked as xfail.
38+
@pytest.mark.xfail(reason=XFAIL_REASON, raises=SystemError, strict=False)
39+
def test_manual_new():
40+
# Repeatedly trigger allocation without initialization (raw malloc'ed) to
41+
# detect uninitialized memory bugs.
42+
for _ in range(32):
43+
# The holder is a pointer variable while the C++ ctor is not called.
44+
vec = m.VectorOwns4PythonObjects.__new__(m.VectorOwns4PythonObjects)
45+
if vec.is_empty():
46+
# The C++ compiler initializes container correctly.
47+
assert vec.size() == 0
48+
else:
49+
raise SystemError(
50+
"Segmentation Fault: The C++ compiler initializes container incorrectly."
51+
)
52+
vec.append(1)
53+
vec.append(2)
54+
vec.append(3)
55+
vec.append(4)
56+
57+
58+
@pytest.mark.skipif(
59+
pybind11_tests.cpp_std_num < 14,
60+
reason="std::{unique_ptr,make_unique} not available in C++11",
61+
)
62+
def test_gc_traverse():
63+
vec = m.create_vector()
64+
vec[3] = (vec, vec)
65+
66+
wr = weakref.ref(vec)
67+
assert wr() is vec
68+
del vec
69+
for _ in range(10):
70+
gc.collect()
71+
assert wr() is None

0 commit comments

Comments
 (0)