From dfcaebf91fa24f860175feb2e860b24b15cdb98a Mon Sep 17 00:00:00 2001 From: Emery Conrad Date: Thu, 22 Oct 2026 09:14:21 -0500 Subject: [PATCH] [cpyrt] Stop the using-directive search at a cycle Mutual using-directives are valid C++ ([namespace.qual]/3). The attribute lookup recursed through them until the stack overflowed. Search each namespace at most once for each lookup. Co-developed-with-the-help-of: Claude Code (Opus 5.5, human in the loop) --- src/cpyrt/CPPScope.cxx | 31 ++++++++++++++++++++----------- test/test_advancedcpp.py | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/cpyrt/CPPScope.cxx b/src/cpyrt/CPPScope.cxx index dd5801ab..202ea2fe 100644 --- a/src/cpyrt/CPPScope.cxx +++ b/src/cpyrt/CPPScope.cxx @@ -505,18 +505,27 @@ static PyObject* meta_getattro(PyObject* pyclass, PyObject* pyname) { } } - // try all outstanding using namespaces in turn to find the attribute (will - // cache locally later; TODO: doing so may cause pathological cases) - for (auto pyref : *klass->fImp.fUsing) { - PyObject* pyuscope = cpyrt_GetWeakRef(pyref); - if (pyuscope) { - attr = PyObject_GetAttr(pyuscope, pyname); - if (!attr) - PyErr_Clear(); - Py_DECREF(pyuscope); + // Search each namespace at most once per lookup ([namespace.qual]/3), so + // that using-directive cycles terminate. + thread_local std::set> searched; + thread_local int depth = 0; + if (searched.emplace(pyclass, name).second) { + ++depth; + // try all outstanding using namespaces in turn to find the attribute + // (will cache locally later; TODO: doing so may cause pathological cases) + for (auto pyref : *klass->fImp.fUsing) { + PyObject* pyuscope = cpyrt_GetWeakRef(pyref); + if (pyuscope) { + attr = PyObject_GetAttr(pyuscope, pyname); + if (!attr) + PyErr_Clear(); + Py_DECREF(pyuscope); + } + if (attr) + break; } - if (attr) - break; + if (--depth == 0) + searched.clear(); } } diff --git a/test/test_advancedcpp.py b/test/test_advancedcpp.py index 6bd7aa7b..b9a5cf4a 100644 --- a/test/test_advancedcpp.py +++ b/test/test_advancedcpp.py @@ -1098,3 +1098,23 @@ def test31_virtual_base_downcast(self): assert isinstance(obj3, ns.Visible3) assert obj3.value == 23 assert obj3.z == 7 + + def test32_using_directive_cycle(self): + """Lookup through cyclic using directives searches each namespace once""" + + import cppjit + + cppjit.cppdef("""\ + namespace udir_cycle_a {} + namespace udir_cycle_p { using namespace udir_cycle_a; int x = 1; } + namespace udir_cycle_a { using namespace udir_cycle_p; int y = 2; }""") + + a = cppjit.gbl.udir_cycle_a + p = cppjit.gbl.udir_cycle_p + assert a.x == 1 + assert p.y == 2 + + with raises(AttributeError): + a.nothere + with raises(AttributeError): + p.nothere