Skip to content

Commit c532da6

Browse files
committed
PYCBC-1816: Fix NULL-derefs and reference leaks in legacy tracing
* NULL-check the set_attribute lookup in the deprecated_request_span constructor. It was stored unchecked, so both add_tag overloads could pass a NULL callable to PyObject_Call and the destructor could Py_DECREF(NULL) * Allow pyObj_span_ to be NULL and tolerate it in every override. A span constructor cannot report a failure: spans are built from the void-returning extract_legacy_span_field helpers in utils.hxx and from start_span, which C++ core calls on its own IO threads, so a throw has nowhere to go. Failures now degrade the span to a no-op and are reported with PyErr_WriteUnraisable, matching the pattern already used in logger.hxx and utils.hxx add_field * DECREF the set_attribute and finish call results. Both were new references that were discarded, leaking one object per tagged operation and one per span end * Route both add_tag overloads through a call_set_attribute helper that NULL-checks the Py_BuildValue result before calling, rather than passing an unchecked args tuple * NULL-check the finish lookup in end() before PyObject_CallObject, so a span without that attribute no longer dereferences NULL and then hits Py_DECREF(NULL) * NULL-check PyTuple_New, PyDict_New, PyUnicode_FromString and PyDict_SetItemString in start_span, and stop handing an unchecked PyObject_Call result to the span constructor, which Py_INCREFs it. A user tracer whose start_span raises previously crashed here; the object it returns is never validated on the Python side * Check the dynamic_pointer_cast result for the parent span in start_span. A parent that is not a deprecated_request_span gave a null shared_ptr that was immediately dereferenced; such a parent now starts a top-level span instead of failing the operation * Report the failure with PyErr_WriteUnraisable instead of returning into C++ core with an exception still set, which would have surfaced as an unrelated error on the next C-API call * Delete the pyObj_get_context_ member, which was never initialized or read Change-Id: I779fcb0174102a3fc1dd74f79cc12e2b2c6642ca Reviewed-on: https://review.couchbase.org/c/couchbase-python-client/+/249624 Reviewed-by: Sergey Avseyev <sergey.avseyev@gmail.com> Tested-by: Build Bot <build@couchbase.com>
1 parent d11df43 commit c532da6

1 file changed

Lines changed: 88 additions & 28 deletions

File tree

‎src/deprecated_tracing.hxx‎

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,16 @@ public:
3838
{
3939
// called by deprecated_request_tracer.start_span & KV/streaming ops (when building a C++ core
4040
// request), so we are confident we have the GIL
41-
Py_INCREF(span);
41+
Py_XINCREF(pyObj_span_);
42+
if (pyObj_span_ == nullptr) {
43+
// the python tracer's start_span() failed, so this span degrades to a no-op
44+
return;
45+
}
4246
pyObj_set_attribute_ = PyObject_GetAttrString(pyObj_span_, "set_attribute");
47+
if (pyObj_set_attribute_ == nullptr) {
48+
// spans are built where we have no way to report a failure, so no-op add_tag() instead
49+
PyErr_WriteUnraisable(pyObj_span_);
50+
}
4351
}
4452

4553
~deprecated_request_span() override
@@ -49,35 +57,41 @@ public:
4957
// The FIX is to not use the legacy (deprecated) tracing, which will no longer be an issue w/
5058
// PYCBC-1746.
5159
PyGILState_STATE state = PyGILState_Ensure();
52-
Py_DECREF(pyObj_set_attribute_);
53-
Py_DECREF(pyObj_span_);
60+
Py_XDECREF(pyObj_set_attribute_);
61+
Py_XDECREF(pyObj_span_);
5462
PyGILState_Release(state);
5563
}
5664

5765
void add_tag(const std::string& name, std::uint64_t value) override
5866
{
5967
PyGILState_STATE state = PyGILState_Ensure();
60-
auto pyObj_args = Py_BuildValue("(sn)", name.c_str(), static_cast<Py_ssize_t>(value));
61-
PyObject_Call(pyObj_set_attribute_, pyObj_args, nullptr);
62-
Py_DECREF(pyObj_args);
68+
call_set_attribute(Py_BuildValue("(sn)", name.c_str(), static_cast<Py_ssize_t>(value)));
6369
PyGILState_Release(state);
6470
}
6571

6672
void add_tag(const std::string& name, const std::string& value) override
6773
{
6874
PyGILState_STATE state = PyGILState_Ensure();
69-
auto pyObj_args = Py_BuildValue("(ss)", name.c_str(), value.c_str());
70-
PyObject_Call(pyObj_set_attribute_, pyObj_args, nullptr);
71-
Py_DECREF(pyObj_args);
75+
call_set_attribute(Py_BuildValue("(ss)", name.c_str(), value.c_str()));
7276
PyGILState_Release(state);
7377
}
7478

7579
void end() override
7680
{
7781
PyGILState_STATE state = PyGILState_Ensure();
78-
auto pyObj_end = PyObject_GetAttrString(pyObj_span_, "finish");
79-
PyObject_CallObject(pyObj_end, nullptr);
80-
Py_DECREF(pyObj_end);
82+
if (pyObj_span_ != nullptr) {
83+
PyObject* pyObj_finish = PyObject_GetAttrString(pyObj_span_, "finish");
84+
if (pyObj_finish == nullptr) {
85+
PyErr_WriteUnraisable(pyObj_span_);
86+
} else {
87+
PyObject* pyObj_res = PyObject_CallObject(pyObj_finish, nullptr);
88+
if (pyObj_res == nullptr) {
89+
PyErr_WriteUnraisable(pyObj_finish);
90+
}
91+
Py_XDECREF(pyObj_res);
92+
Py_DECREF(pyObj_finish);
93+
}
94+
}
8195
PyGILState_Release(state);
8296
}
8397

@@ -87,9 +101,27 @@ public:
87101
}
88102

89103
private:
104+
// Steals pyObj_args, which is NULL when Py_BuildValue failed. The caller must hold the GIL.
105+
void call_set_attribute(PyObject* pyObj_args)
106+
{
107+
if (pyObj_args == nullptr || pyObj_set_attribute_ == nullptr) {
108+
// add_tag() returns into C++ core, so an error must never be left pending
109+
if (PyErr_Occurred() != nullptr) {
110+
PyErr_WriteUnraisable(pyObj_span_);
111+
}
112+
Py_XDECREF(pyObj_args);
113+
return;
114+
}
115+
PyObject* pyObj_res = PyObject_Call(pyObj_set_attribute_, pyObj_args, nullptr);
116+
if (pyObj_res == nullptr) {
117+
PyErr_WriteUnraisable(pyObj_set_attribute_);
118+
}
119+
Py_XDECREF(pyObj_res);
120+
Py_DECREF(pyObj_args);
121+
}
122+
90123
PyObject* pyObj_span_;
91-
PyObject* pyObj_set_attribute_;
92-
PyObject* pyObj_get_context_;
124+
PyObject* pyObj_set_attribute_{ nullptr };
93125
};
94126

95127
class deprecated_request_tracer : public cbtracing::request_tracer
@@ -130,21 +162,9 @@ public:
130162
// cache spans and periodically (or just when asked) grab the GIL and create them. However,
131163
// lets do this first, then think about optimizations
132164
PyGILState_STATE state = PyGILState_Ensure();
133-
PyObject* pyObj_name = PyUnicode_FromString(name.c_str());
134-
PyObject* pyObj_args = PyTuple_New(0);
135-
PyObject* pyObj_kwargs = PyDict_New();
136-
PyDict_SetItemString(pyObj_kwargs, "name", pyObj_name);
137-
if (parent) {
138-
auto pyObj_parent =
139-
std::dynamic_pointer_cast<pycbc::deprecated_request_span>(parent)->py_span();
140-
PyDict_SetItemString(pyObj_kwargs, "parent", pyObj_parent);
141-
}
142-
auto pyObj_span = PyObject_Call(pyObj_start_span_, pyObj_args, pyObj_kwargs);
165+
PyObject* pyObj_span = call_start_span(name, parent);
143166
auto retval = std::make_shared<pycbc::deprecated_request_span>(pyObj_span, parent);
144-
Py_DECREF(pyObj_name);
145-
Py_DECREF(pyObj_args);
146-
Py_DECREF(pyObj_kwargs);
147-
Py_DECREF(pyObj_span);
167+
Py_XDECREF(pyObj_span);
148168
PyGILState_Release(state);
149169
return retval;
150170
}
@@ -158,6 +178,46 @@ public:
158178
}
159179

160180
private:
181+
// Adds the python span behind parent, if there is one, to the kwargs. Returns false w/ an error
182+
// set. The caller must hold the GIL.
183+
static bool maybe_add_parent(PyObject* pyObj_kwargs,
184+
const std::shared_ptr<cbtracing::request_span>& parent)
185+
{
186+
if (!parent) {
187+
return true;
188+
}
189+
auto pycbc_parent = std::dynamic_pointer_cast<pycbc::deprecated_request_span>(parent);
190+
if (pycbc_parent == nullptr || pycbc_parent->py_span() == nullptr) {
191+
// not a legacy span, or a no-op one, so start a top-level span instead of failing the op
192+
return true;
193+
}
194+
return PyDict_SetItemString(pyObj_kwargs, "parent", pycbc_parent->py_span()) == 0;
195+
}
196+
197+
// Calls the python tracer's start_span(). Returns a new reference, or NULL w/ the failure already
198+
// reported. The caller must hold the GIL.
199+
PyObject* call_start_span(const std::string& name,
200+
const std::shared_ptr<cbtracing::request_span>& parent)
201+
{
202+
PyObject* pyObj_span = nullptr;
203+
PyObject* pyObj_args = PyTuple_New(0);
204+
PyObject* pyObj_kwargs = PyDict_New();
205+
PyObject* pyObj_name = PyUnicode_FromString(name.c_str());
206+
if (pyObj_args != nullptr && pyObj_kwargs != nullptr && pyObj_name != nullptr &&
207+
PyDict_SetItemString(pyObj_kwargs, "name", pyObj_name) == 0 &&
208+
maybe_add_parent(pyObj_kwargs, parent)) {
209+
pyObj_span = PyObject_Call(pyObj_start_span_, pyObj_args, pyObj_kwargs);
210+
}
211+
if (pyObj_span == nullptr) {
212+
// start_span() returns into C++ core, so an error must never be left pending
213+
PyErr_WriteUnraisable(pyObj_start_span_);
214+
}
215+
Py_XDECREF(pyObj_name);
216+
Py_XDECREF(pyObj_args);
217+
Py_XDECREF(pyObj_kwargs);
218+
return pyObj_span;
219+
}
220+
161221
PyObject* pyObj_tracer_;
162222
PyObject* pyObj_start_span_;
163223
};

0 commit comments

Comments
 (0)