From 70ef38b3e7c0297e58a4eeafd59a02a9a8261ee9 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sat, 31 Oct 2026 18:19:07 -0400 Subject: [PATCH 1/3] test: reproduce unchecked sequence end errors --- tests/test_pytypes.cpp | 15 +++++++++++++++ tests/test_pytypes.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp index b500eb7972..aa8e212cd7 100644 --- a/tests/test_pytypes.cpp +++ b/tests/test_pytypes.cpp @@ -461,6 +461,21 @@ TEST_SUBMODULE(pytypes, m) { return py::tuple(); }); + m.def("capi_sequence_end", [](const py::sequence &seq) { + py::detail::sequence_iterator end; + try { + end = seq.end(); + } catch (const py::error_already_set &) { + throw; + } + if (PyErr_Occurred()) { + PyErr_Clear(); + PyErr_SetString(PyExc_AssertionError, "sequence::end returned with a pending error"); + throw py::error_already_set(); + } + return end - seq.begin(); + }); + m.def("accessor_assignment", []() { auto l = py::list(1); l[0] = 0; diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index 3c7b31e093..8ed9cc9528 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -837,6 +837,47 @@ def test_cpp_iterators(): assert m.passed_iterator(iter((-7, 3))) == -4 +def test_capi_sequence_end_error(): + error = RuntimeError("sequence length failed") + + class BadLength: + def __getitem__(self, index): + raise AssertionError("end must not retrieve items") + + def __len__(self): + raise error + + with pytest.raises(RuntimeError) as excinfo: + m.capi_sequence_end(BadLength()) + assert excinfo.value is error + + +def test_capi_sequence_end_negative_length(): + class NegativeLength: + def __getitem__(self, index): + raise AssertionError("end must not retrieve items") + + def __len__(self): + return -1 # noqa: PLE0303 - deliberately invalid length + + with pytest.raises(ValueError): + m.capi_sequence_end(NegativeLength()) + + +@pytest.mark.parametrize("values", [[], [4, 7]]) +@pytest.mark.parametrize("kind", [list, tuple, "custom"]) +def test_capi_sequence_end_valid(values, kind): + class CustomSequence: + def __getitem__(self, index): + return values[index] + + def __len__(self): + return len(values) + + sequence = CustomSequence() if kind == "custom" else kind(values) + assert m.capi_sequence_end(sequence) == len(values) + + def test_implementation_details(): lst = [39, 43, 92, 49, 22, 29, 93, 98, 26, 57, 8] tup = tuple(lst) From 08453b7229ff574c8310f56bfe7cc4e6d64b0108 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sat, 31 Oct 2026 18:26:33 -0400 Subject: [PATCH 2/3] fix: propagate sequence end length errors --- include/pybind11/pytypes.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 88de78d024..e820417bd9 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -2032,7 +2032,13 @@ class sequence : public object { return object::operator[](std::forward(o)); } detail::sequence_iterator begin() const { return {*this, 0}; } - detail::sequence_iterator end() const { return {*this, PySequence_Size(m_ptr)}; } + detail::sequence_iterator end() const { + ssize_t result = PySequence_Size(m_ptr); + if (result == -1) { + throw error_already_set(); + } + return {*this, result}; + } }; class list : public object { From 6f899ec0d5022c34af51f2d71e2ab5ef1635b62c Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 1 Nov 2026 23:23:17 -0500 Subject: [PATCH 3/3] test: preserve sequence end error causes --- tests/test_pytypes.cpp | 3 +-- tests/test_pytypes.py | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp index aa8e212cd7..2764645466 100644 --- a/tests/test_pytypes.cpp +++ b/tests/test_pytypes.cpp @@ -469,8 +469,7 @@ TEST_SUBMODULE(pytypes, m) { throw; } if (PyErr_Occurred()) { - PyErr_Clear(); - PyErr_SetString(PyExc_AssertionError, "sequence::end returned with a pending error"); + py::raise_from(PyExc_AssertionError, "sequence::end returned with a pending error"); throw py::error_already_set(); } return end - seq.begin(); diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index 8ed9cc9528..b633c1cde0 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -837,8 +837,9 @@ def test_cpp_iterators(): assert m.passed_iterator(iter((-7, 3))) == -4 -def test_capi_sequence_end_error(): - error = RuntimeError("sequence length failed") +@pytest.mark.parametrize("error_type", [RuntimeError, KeyboardInterrupt, SystemExit]) +def test_capi_sequence_end_error(error_type): + error = error_type("sequence length failed") class BadLength: def __getitem__(self, index): @@ -847,9 +848,10 @@ def __getitem__(self, index): def __len__(self): raise error - with pytest.raises(RuntimeError) as excinfo: + with pytest.raises(error_type) as excinfo: m.capi_sequence_end(BadLength()) assert excinfo.value is error + assert "__len__" in [entry.name for entry in excinfo.traceback] def test_capi_sequence_end_negative_length():