From 62d3f9bc23d7eb40edb0f249ea6bac3da99760d0 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sat, 14 Nov 2026 15:04:35 -0500 Subject: [PATCH] fix: keep tuple_caster items alive until the subcasters are used tuple_caster loaded each subcaster from a temporary item, but uses the subcasters later. A subcaster can refer to its item, so a sequence that creates items on access caused a use-after-free. Assisted-by: ClaudeCode:claude-fable-5-2 --- include/pybind11/cast.h | 11 +++++++++-- tests/test_builtin_casters.cpp | 19 +++++++++++++++++++ tests/test_builtin_casters.py | 13 +++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 79e743d9bb..af03986a27 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -819,14 +819,19 @@ class tuple_caster { static constexpr bool load_impl(const sequence &, bool, index_sequence<>) { return true; } + handle load_item(const sequence &seq, size_t index) { + items[index] = accessor_policies::sequence_item::get(seq, index); + return items[index]; + } + template bool load_impl(const sequence &seq, bool convert, index_sequence) { #ifdef __cpp_fold_expressions - if ((... || !std::get(subcasters).load(seq[Is], convert))) { + if ((... || !std::get(subcasters).load(load_item(seq, Is), convert))) { return false; } #else - for (bool r : {std::get(subcasters).load(seq[Is], convert)...}) { + for (bool r : {std::get(subcasters).load(load_item(seq, Is), convert)...}) { if (!r) { return false; } @@ -859,6 +864,8 @@ class tuple_caster { } Tuple...> subcasters; + // A subcaster can refer to its item, and the sequence does not necessarily keep it alive. + std::array items; }; template diff --git a/tests/test_builtin_casters.cpp b/tests/test_builtin_casters.cpp index b7599615e3..cbcbf3457e 100644 --- a/tests/test_builtin_casters.cpp +++ b/tests/test_builtin_casters.cpp @@ -11,6 +11,8 @@ #include "pybind11_tests.h" +#include + struct ConstRefCasted { int tag; }; @@ -256,6 +258,23 @@ TEST_SUBMODULE(builtin_casters, m) { }, "Return a triple in reversed order"); m.def("empty_tuple", []() { return std::tuple<>(); }); + + // test_tuple_items_created_on_the_fly + // The copy constructor records if the source instance was alive. + struct TupleItem { + explicit TupleItem(int) { alive().insert(this); } + TupleItem(const TupleItem &other) : source_was_alive{alive().count(&other) != 0} {} + ~TupleItem() { alive().erase(this); } + static std::set &alive() { + static std::set instances; + return instances; + } + bool source_was_alive = true; + }; + py::class_(m, "TupleItem").def(py::init()); + m.def("pair_item_sources_were_alive", [](const std::pair &input) { + return std::make_pair(input.first.source_was_alive, input.second.source_was_alive); + }); static std::pair lvpair; static std::tuple lvtuple; static std::pair>> diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py index 6b8be71221..c37c14d16c 100644 --- a/tests/test_builtin_casters.py +++ b/tests/test_builtin_casters.py @@ -467,6 +467,19 @@ def test_tuple(doc): assert m.int_string_pair() == (2, "items") +def test_tuple_items_created_on_the_fly(): + """Each item is kept alive until its value was copied (no use-after-free).""" + + class OnTheFly: + def __len__(self): + return 2 + + def __getitem__(self, index): + return m.TupleItem(index) # The sequence does not retain the item. + + assert m.pair_item_sources_were_alive(OnTheFly()) == (True, True) + + def test_builtins_cast_return_none(): """Casters produced with PYBIND11_TYPE_CASTER() should convert nullptr to None""" assert m.return_none_string() is None