Skip to content

Commit 070213b

Browse files
committed
Ensure that HeldClass bound methods don't hit released memory.
HeldClass instances have a different lifetime than regular classes - things like bound methods on them store a reference to them via pointer. This allows us to do things like l = ListOf(SomeHeldClass)([...]) l[10].f() and allow 'f' to modify the contents of the list in interepreted as well as compiled code, even though the list is holding H as packed memory. This is, of course, very dangerous! Such a reference will be invalidated by resizing the list, for instance (really, anything that moves the memory), following the standard semantics of C++. However, we also had a bug where you could just write SomeHeldClass().f() and crash, because the intermediate 'f' was not holding SomeHeldClass() alive. This change fixes this by doing two things: (1) we ensure that bound methods keep the object being bound alive for the duration of the instruction in the parent frame. This follows the same semantic model we have for temporary 'refTo' instances (2) we cause 'f' to keep the temporary alive, and then throw an exception if we are the last refcount to the object alive.
1 parent 8b734ed commit 070213b

13 files changed

+167
-34
lines changed

typed_python/FunctionType.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ PyObject* Function::Overload::buildFunctionObj(Type* closureType, instance_ptr c
172172
newCellContents.steal(
173173
PyInstance::extractPythonObject(
174174
((TypedCellType*)bindingValue.type())->get(bindingValue.data()),
175-
((TypedCellType*)bindingValue.type())->getHeldType()
175+
((TypedCellType*)bindingValue.type())->getHeldType(),
176+
false
176177
)
177178
);
178179
} else {

typed_python/Instance.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ Instance::Instance(instance_ptr p, Type* t) : mLayout(nullptr) {
8484
Instance::~Instance() {
8585
if (mLayout && mLayout->refcount.fetch_sub(1) == 1) {
8686
mLayout->type->destroy(mLayout->data);
87+
88+
if (mLayout->type->bytecount() >= 8) {
89+
// write a terminator so that we can see that this memory was destroyed.
90+
((uint64_t*)mLayout->data)[0] = 0xdeadbeef;
91+
}
92+
8793
tp_free(mLayout);
8894
}
8995
}

typed_python/PyAlternativeInstance.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ PyObject* PyAlternativeInstance::tp_getattr_concrete(PyObject* pyAttrName, const
166166
return extractPythonObject(
167167
heldT->eltPtr(heldData, ix),
168168
heldT->getTypes()[ix]
169-
);
169+
);
170170
}
171171

172172
return PyInstance::tp_getattr_concrete(pyAttrName, attrName);

typed_python/PyBoundMethodInstance.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ BoundMethod* PyBoundMethodInstance::type() {
2222
}
2323

2424
PyObject* PyBoundMethodInstance::tp_call_concrete(PyObject* args, PyObject* kwargs) {
25+
if (mKeepalive) {
26+
if (mKeepalive->ob_refcnt == 1) {
27+
PyErr_Format(
28+
PyExc_RuntimeError,
29+
"BoundMethod pointing at HeldClass 'self' would have crashed in compiled code."
30+
);
31+
32+
return NULL;
33+
}
34+
}
35+
2536
Function* f = type()->getFunction();
2637
Type* firstArgType = type()->getFirstArgType();
2738

typed_python/PyClassInstance.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,9 +564,19 @@ PyObject* PyClassInstance::tpGetattrGeneric(
564564
throw std::runtime_error("BoundMethod of HeldClass should take a RefTo");
565565
}
566566

567-
return PyInstance::initializePythonRepresentation(method, [&](instance_ptr methodData) {
567+
PyObject* res = PyInstance::initializePythonRepresentation(method, [&](instance_ptr methodData) {
568568
method->copy_constructor(methodData, (instance_ptr)&heldData);
569569
});
570+
571+
// the function reference holds 'self' as a RefTo. If 'self' is a temporary that
572+
// doesn't exist in a variable (say, it was returned as a new object from a
573+
// function) then this refto could end up pointing at the wrong data.
574+
((PyInstance*)res)->setKeepalive(self);
575+
576+
// ensure that self is kept alive for the duration of the instruction.
577+
PyTemporaryReferenceTracer::keepaliveForCurrentInstruction(self);
578+
579+
return res;
570580
}
571581
}
572582

typed_python/PyFunctionInstance.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ std::pair<bool, PyObject*> PyFunctionInstance::dispatchFunctionCallToCompiledSpe
569569
}
570570
});
571571

572-
return std::pair<bool, PyObject*>(true, (PyObject*)extractPythonObject(result.data(), result.type()));
572+
return std::pair<bool, PyObject*>(true, (PyObject*)extractPythonObject(result.data(), result.type(), false));
573573
}
574574

575575

typed_python/PyInstance.cpp

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ PyMethodDef* PyInstance::typeMethodsConcrete(Type* t) {
120120
void PyInstance::tp_dealloc(PyObject* self) {
121121
PyInstance* wrapper = (PyInstance*)self;
122122

123-
wrapper->mContainingInstance.~Instance();
123+
wrapper->teardown();
124124

125125
Py_TYPE(self)->tp_free((PyObject*)self);
126126
}
@@ -277,14 +277,9 @@ PyObject* PyInstance::extractPythonObject(instance_ptr data, Type* eltType, bool
277277
// this allows us to mimic the behavior of the compiler when we're
278278
// using these objects: by default, we never get naked reftos anywhere
279279
// we can find them.
280-
PyObject* res = PyInstance::initializeTemporaryRef(eltType, data);
281-
282-
PyThreadState *tstate = PyThreadState_GET();
283-
PyFrameObject *f = tstate->frame;
284280

285-
if (f) {
286-
PyTemporaryReferenceTracer::traceObject(res, f);
287-
}
281+
PyObject* res = PyInstance::initializeTemporaryRef(eltType, data);
282+
PyTemporaryReferenceTracer::traceObject(res);
288283

289284
return res;
290285
}
@@ -315,8 +310,8 @@ PyObject* PyInstance::extractPythonObject(instance_ptr data, Type* eltType, bool
315310
});
316311
}
317312

318-
PyObject* PyInstance::extractPythonObject(const Instance& instance) {
319-
return extractPythonObject(instance.data(), instance.type());
313+
PyObject* PyInstance::extractPythonObject(const Instance& instance, bool createTemporaryRef) {
314+
return extractPythonObject(instance.data(), instance.type(), createTemporaryRef);
320315
}
321316

322317
PyObject* PyInstance::extractPythonObjectConcrete(Type* eltType, instance_ptr data) {

typed_python/PyInstance.hpp

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,41 @@ class PyInstance {
7878
// we may be a temporary ref to a type
7979
instance_ptr mTemporaryRefTo;
8080

81+
PyObject* mKeepalive;
82+
8183
// initialize our fields after we have called 'tp_alloc'
8284
void initializeEmpty() {
8385
mIteratorFlag = -1;
8486
mIteratorOffset = -1;
8587
mContainerSize = -1;
8688
mTemporaryRefTo = nullptr;
89+
mKeepalive = nullptr;
8790

8891
new (&mContainingInstance) Instance();
8992
}
9093

94+
// called when this python object is destroyed. Gives us a chance to do cleanup actions
95+
void teardown() {
96+
mContainingInstance.~Instance();
97+
98+
if (mKeepalive) {
99+
Py_DECREF(mKeepalive);
100+
mKeepalive = nullptr;
101+
}
102+
}
103+
104+
void setKeepalive(PyObject* toKeepAlive) {
105+
if (toKeepAlive) {
106+
Py_INCREF(toKeepAlive);
107+
}
108+
109+
if (mKeepalive) {
110+
Py_DECREF(mKeepalive);
111+
}
112+
113+
mKeepalive = toKeepAlive;
114+
}
115+
91116
void resolveTemporaryReference();
92117

93118
template<class T>
@@ -347,8 +372,8 @@ class PyInstance {
347372
mContainingInstance = Instance(type, i);
348373
}
349374

350-
static PyObject* fromInstance(const Instance& instance) {
351-
return extractPythonObject(instance.data(), instance.type());
375+
static PyObject* fromInstance(const Instance& instance, bool createTemporaryRef=true) {
376+
return extractPythonObject(instance.data(), instance.type(), createTemporaryRef);
352377
}
353378

354379
PyInstance* duplicate() {
@@ -403,7 +428,7 @@ class PyInstance {
403428
//instances.
404429
static PyObject* extractPythonObject(instance_ptr data, Type* eltType, bool createTemporaryRef=true);
405430

406-
static PyObject* extractPythonObject(const Instance& instance);
431+
static PyObject* extractPythonObject(const Instance& instance, bool createTemporaryRef=true);
407432

408433
//if we have a python representation that we want to use for this object, override and return not-NULL.
409434
//otherwise, this version takes over and returns a PyInstance wrapper for the object

typed_python/PyTemporaryReferenceTracer.cpp

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
#include "PyTemporaryReferenceTracer.hpp"
1818

1919

20-
int PyTemporaryReferenceTracer::globalTraceFun(PyObject* obj, PyFrameObject* frame, int what, PyObject* arg) {
20+
int PyTemporaryReferenceTracer::globalTraceFun(PyObject* dummyObj, PyFrameObject* frame, int what, PyObject* arg) {
2121
if (frame != globalTracer.mostRecentEmptyFrame) {
2222
auto it = globalTracer.frameToHandles.find(frame);
2323

@@ -26,9 +26,12 @@ int PyTemporaryReferenceTracer::globalTraceFun(PyObject* obj, PyFrameObject* fra
2626
} else {
2727
globalTracer.mostRecentEmptyFrame = nullptr;
2828

29-
for (auto obj: it->second) {
30-
((PyInstance*)obj)->resolveTemporaryReference();
31-
decref(obj);
29+
for (auto objAndAction: it->second) {
30+
if (objAndAction.second == TraceAction::ConvertTemporaryReference) {
31+
((PyInstance*)objAndAction.first)->resolveTemporaryReference();
32+
}
33+
34+
decref(objAndAction.first);
3235
}
3336

3437
globalTracer.frameToHandles.erase(it);
@@ -54,25 +57,58 @@ int PyTemporaryReferenceTracer::globalTraceFun(PyObject* obj, PyFrameObject* fra
5457
return res;
5558
}
5659

60+
void PyTemporaryReferenceTracer::installGlobalTraceHandlerIfNecessary() {
61+
// ensure that the global trace handler is installed
62+
PyThreadState *tstate = PyThreadState_GET();
63+
64+
// this swallows the reference we're holding on 'tracer' into the function itself
65+
if (tstate->c_tracefunc != globalTraceFun) {
66+
globalTracer.priorTraceFunc = tstate->c_tracefunc;
67+
globalTracer.priorTraceFuncArg = incref(tstate->c_traceobj);
68+
69+
PyEval_SetTrace(PyTemporaryReferenceTracer::globalTraceFun, nullptr);
70+
}
71+
}
5772

5873
void PyTemporaryReferenceTracer::traceObject(PyObject* o, PyFrameObject* f) {
5974
// mark that we're going to trace
60-
globalTracer.frameToHandles[f].push_back(incref(o));
75+
globalTracer.frameToHandles[f].push_back(std::make_pair(incref(o), TraceAction::ConvertTemporaryReference));
6176

62-
// reset the
6377
if (globalTracer.mostRecentEmptyFrame == f) {
6478
globalTracer.mostRecentEmptyFrame = nullptr;
6579
}
6680

81+
installGlobalTraceHandlerIfNecessary();
82+
}
83+
84+
void PyTemporaryReferenceTracer::keepaliveForCurrentInstruction(PyObject* o, PyFrameObject* f) {
85+
// mark that we're going to trace
86+
globalTracer.frameToHandles[f].push_back(std::make_pair(incref(o), TraceAction::Decref));
87+
88+
if (globalTracer.mostRecentEmptyFrame == f) {
89+
globalTracer.mostRecentEmptyFrame = nullptr;
90+
}
91+
92+
installGlobalTraceHandlerIfNecessary();
93+
}
94+
95+
void PyTemporaryReferenceTracer::traceObject(PyObject* o) {
6796
PyThreadState *tstate = PyThreadState_GET();
97+
PyFrameObject *f = tstate->frame;
6898

69-
// this swallows the reference we're holding on 'tracer' into the function itself
70-
if (tstate->c_tracefunc != globalTraceFun) {
71-
globalTracer.priorTraceFunc = tstate->c_tracefunc;
72-
globalTracer.priorTraceFuncArg = incref(tstate->c_traceobj);
99+
if (f) {
100+
PyTemporaryReferenceTracer::traceObject(o, f);
101+
}
102+
}
73103

74-
PyEval_SetTrace(PyTemporaryReferenceTracer::globalTraceFun, nullptr);
104+
void PyTemporaryReferenceTracer::keepaliveForCurrentInstruction(PyObject* o) {
105+
PyThreadState *tstate = PyThreadState_GET();
106+
PyFrameObject *f = tstate->frame;
107+
108+
if (f) {
109+
PyTemporaryReferenceTracer::keepaliveForCurrentInstruction(o, f);
75110
}
76111
}
77112

113+
78114
PyTemporaryReferenceTracer PyTemporaryReferenceTracer::globalTracer;

typed_python/PyTemporaryReferenceTracer.hpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
#include "PyInstance.hpp"
2020
#include <unordered_map>
2121

22+
enum class TraceAction {
23+
ConvertTemporaryReference,
24+
Decref
25+
};
26+
2227
class PyTemporaryReferenceTracer {
2328
public:
2429
PyTemporaryReferenceTracer() :
@@ -27,7 +32,7 @@ class PyTemporaryReferenceTracer {
2732
priorTraceFuncArg(nullptr)
2833
{}
2934

30-
std::unordered_map<PyFrameObject*, std::vector<PyObject*> > frameToHandles;
35+
std::unordered_map<PyFrameObject*, std::vector<std::pair<PyObject*, TraceAction> > > frameToHandles;
3136

3237
// the most recent frame we touched that has nothing in it
3338
PyFrameObject* mostRecentEmptyFrame;
@@ -43,4 +48,12 @@ class PyTemporaryReferenceTracer {
4348
// the next time we have an instruction in 'frame', trigger 'o' to become
4449
// a non-temporary reference
4550
static void traceObject(PyObject* o, PyFrameObject* frame);
51+
52+
static void traceObject(PyObject* o);
53+
54+
static void installGlobalTraceHandlerIfNecessary();
55+
56+
static void keepaliveForCurrentInstruction(PyObject* o);
57+
58+
static void keepaliveForCurrentInstruction(PyObject* o, PyFrameObject* frame);
4659
};

0 commit comments

Comments
 (0)