Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Include/internal/pycore_regions.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ extern "C" {
PyObject* _Py_MakeImmutable(PyObject* obj);
#define Py_MakeImmutable(op) _Py_MakeImmutable(_PyObject_CAST(op))

PyObject* _Py_InvariantSrcFailure(void);
#define Py_InvariantSrcFailure() _Py_InvariantSrcFailure()

PyObject* _Py_InvariantTgtFailure(void);
#define Py_InvariantTgtFailure() _Py_InvariantTgtFailure()

PyObject* _Py_EnableInvariant(void);
#define Py_EnableInvariant() _Py_EnableInvariant()

#ifdef NDEBUG
#define _Py_VPYDBG(fmt, ...)
#define _Py_VPYDBGPRINT(fmt, ...)
Expand All @@ -25,6 +34,8 @@ PyObject* _Py_MakeImmutable(PyObject* obj);
#define _Py_VPYDBGPRINT(op) PyObject_Print(_PyObject_CAST(op), stdout, 0)
#endif

int _Py_CheckRegionInvariant(PyThreadState *tstate);

#ifdef __cplusplus
}
#endif
Expand Down
168 changes: 168 additions & 0 deletions Objects/regions.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <stdbool.h>
#include <stdio.h>
#include "pycore_dict.h"
#include "pycore_interp.h"
#include "pycore_object.h"
#include "pycore_regions.h"

Expand Down Expand Up @@ -88,6 +89,170 @@ bool is_c_wrapper(PyObject* obj){
return PyCFunction_Check(obj) || Py_IS_TYPE(obj, &_PyMethodWrapper_Type) || Py_IS_TYPE(obj, &PyWrapperDescr_Type);
}

/**
* Global status for performing the region check.
*/
bool do_region_check = false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably over thinking this (or too early) but should these bits be part of the per-interpreter state? Or will the invariant check always stop all subinterpreters and then check? I am thinking we (eventually) need to support the existance of multiple threads while we are checking.

What I am thinking is that it may be wise to access these through an indirection already which allows us to lift these globals into something else later.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure. This is very localised code, so lets design when we get there.


// The src object for an edge that invalidated the invariant.
PyObject* error_src = Py_None;

// The tgt object for an edge that invalidated the invariant.
PyObject* error_tgt = Py_None;

// Once an error has occurred this is used to surpress further checking
bool error_occurred = false;


/**
* Enable the region check.
*/
void notify_regions_in_use(void)
{
// Do not re-enable, if we have detected a fault.
if (!error_occurred)
do_region_check = true;
}

PyObject* _Py_EnableInvariant(void)
{
// Disable failure as program has explicitly requested invariant to be checked again.
error_occurred = false;
// Re-enable region check
do_region_check = true;
return Py_None;
}

/**
* Set the global variables for a failure.
* This allows the interpreter to inspect what has failed.
*/
void set_failed_edge(PyObject* src, PyObject* tgt, const char* msg)
{
Py_DecRef(error_src);
Py_IncRef(src);
error_src = src;
Py_DecRef(error_tgt);
Py_IncRef(tgt);
error_tgt = tgt;
printf("Error: Invalid edge %p -> %p: %s\n", src, tgt, msg);
// We have discovered a failure.
// Disable region check, until the program switches it back on.
do_region_check = false;
error_occurred = true;
}

PyObject* _Py_InvariantSrcFailure(void)
{
return Py_NewRef(error_src);
}

PyObject* _Py_InvariantTgtFailure(void)
{
return Py_NewRef(error_tgt);
}


// Lifted from gcmodule.c
typedef struct _gc_runtime_state GCState;
#define GEN_HEAD(gcstate, n) (&(gcstate)->generations[n].head)
#define GC_NEXT _PyGCHead_NEXT
#define GC_PREV _PyGCHead_PREV
#define FROM_GC(g) ((PyObject *)(((char *)(g))+sizeof(PyGC_Head)))


/* A traversal callback for _Py_CheckRegionInvariant.
- op is the target of the reference we are checking, and
- parent is the source of the reference we are checking.
*/
static int
visit_invariant_check(PyObject *op, void *parent)
{
PyObject *src_op = _PyObject_CAST(parent);
// Check Immutable only reaches immutable
if ((src_op->ob_region == _Py_IMMUTABLE)
&& (op->ob_region != _Py_IMMUTABLE))
{
set_failed_edge(src_op, op, "Destination is not immutable");
return 0;
}
// TODO: More checks to go here as we add more region
// properties.

return 0;
}

/**
* This uses checks that the region topology is valid.
*
* It is currently implemented using the GC data. This
* means that not all objects are traversed as some objects
* are considered to not participate in cycles, and hence
* do not need to be understood for the cycle detector.
*
* This is not ideal for the region invariant, but is a good
* first approximation. We could actually walk the heap
* in a subsequent more elaborate invariant check.
*
* Returns non-zero if the invariant is violated.
*/
int _Py_CheckRegionInvariant(PyThreadState *tstate)
{
// Check if we should perform the region invariant check
if(!do_region_check){
return 0;
}

// Use the GC data to find all the objects, and traverse them to
// confirm all their references satisfy the region invariant.
GCState *gcstate = &tstate->interp->gc;

// There is an cyclic doubly linked list per generation of all the objects
// in that generation.
for (int i = NUM_GENERATIONS-1; i >= 0; i--) {
PyGC_Head *containers = GEN_HEAD(gcstate, i);
PyGC_Head *gc = GC_NEXT(containers);
// Walk doubly linked list of objects.
for (; gc != containers; gc = GC_NEXT(gc)) {
PyObject *op = FROM_GC(gc);
// Local can point to anything. No invariant needed
if (op->ob_region == _Py_DEFAULT_REGION)
continue;
// Functions are complex.
// Removing from invariant initially.
// TODO provide custom traverse here.
if (PyFunction_Check(op))
continue;

// TODO the immutable code ignores c_wrappers
// review if this is correct.
if (is_c_wrapper(op))
continue;

// Use traverse proceduce to visit each field of the object.
traverseproc traverse = Py_TYPE(op)->tp_traverse;
(void) traverse(op,
(visitproc)visit_invariant_check,
op);

// Also need to visit the type of the object
// As this isn't covered by the traverse.
PyObject* type_op = PyObject_Type(op);
visit_invariant_check(op, type_op);
Py_DECREF(type_op);

// If we detected an error, stop so we don't
// write too much.
// TODO: The first error might not be the most useful.
// So might not need to build all error edges as a structure.
if (error_occurred)
return 1;
}
}

return 0;
}

#define _Py_VISIT_FUNC_ATTR(attr, frontier) do { \
if(attr != NULL && !_Py_IsImmutable(attr)){ \
Py_INCREF(attr); \
Expand Down Expand Up @@ -377,6 +542,9 @@ int _makeimmutable_visit(PyObject* obj, void* frontier)

PyObject* _Py_MakeImmutable(PyObject* obj)
{
// We have started using regions, so notify to potentially enable checks.
notify_regions_in_use();

_Py_VPYDBG(">> makeimmutable(");
_Py_VPYDBGPRINT(obj);
_Py_VPYDBG(") region: %lu rc: %ld\n", Py_REGION(obj), Py_REFCNT(obj));
Expand Down
45 changes: 44 additions & 1 deletion Python/bltinmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2770,11 +2770,51 @@ Make 'obj' and its entire reachable object graph immutable.

static PyObject *
builtin_makeimmutable(PyObject *module, PyObject *obj)
/*[clinic end generated code: output=4e665122542dfd24 input=21a50256fa4fb099]*/
/*[clinic end generated code: output=4e665122542dfd24 input=bec4cf1797c848d4]*/
{
return Py_MakeImmutable(obj);
}

/*[clinic input]
invariant_failure_src as builtin_invariantsrcfailure

Find the source of an invariant failure.
[clinic start generated code]*/

static PyObject *
builtin_invariantsrcfailure_impl(PyObject *module)
/*[clinic end generated code: output=8830901cbbefe8ba input=0266aae8308be0a4]*/
{
return Py_InvariantSrcFailure();
}

/*[clinic input]
invariant_failure_tgt as builtin_invarianttgtfailure

Find the target of an invariant failure.
[clinic start generated code]*/

static PyObject *
builtin_invarianttgtfailure_impl(PyObject *module)
/*[clinic end generated code: output=f7c9cd7cb737bd13 input=9c79a563d1eb52f9]*/
{
return Py_InvariantTgtFailure();
}

/*[clinic input]
enableinvariant as builtin_enableinvariant

Enable the checking of the region invariant.
[clinic start generated code]*/

static PyObject *
builtin_enableinvariant_impl(PyObject *module)
/*[clinic end generated code: output=a3a27509957788c2 input=cf5922b1eb45ef0e]*/
{
return Py_EnableInvariant();
}


typedef struct {
PyObject_HEAD
Py_ssize_t tuplesize;
Expand Down Expand Up @@ -3061,6 +3101,7 @@ static PyMethodDef builtin_methods[] = {
BUILTIN_DELATTR_METHODDEF
BUILTIN_DIR_METHODDEF
BUILTIN_DIVMOD_METHODDEF
BUILTIN_ENABLEINVARIANT_METHODDEF
BUILTIN_EVAL_METHODDEF
BUILTIN_EXEC_METHODDEF
BUILTIN_FORMAT_METHODDEF
Expand All @@ -3075,6 +3116,8 @@ static PyMethodDef builtin_methods[] = {
BUILTIN_ISSUBCLASS_METHODDEF
BUILTIN_ISIMMUTABLE_METHODDEF
BUILTIN_MAKEIMMUTABLE_METHODDEF
BUILTIN_INVARIANTSRCFAILURE_METHODDEF
BUILTIN_INVARIANTTGTFAILURE_METHODDEF
BUILTIN_ITER_METHODDEF
BUILTIN_AITER_METHODDEF
BUILTIN_LEN_METHODDEF
Expand Down
6 changes: 6 additions & 0 deletions Python/ceval_gil.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "pycore_initconfig.h" // _PyStatus_OK()
#include "pycore_interp.h" // _Py_RunGC()
#include "pycore_pymem.h" // _PyMem_IsPtrFreed()
#include "pycore_regions.h" // _Py_CheckRegionInvariant()

/*
Notes about the implementation:
Expand Down Expand Up @@ -1056,6 +1057,11 @@ _Py_HandlePending(PyThreadState *tstate)
struct _ceval_runtime_state *ceval = &runtime->ceval;
struct _ceval_state *interp_ceval_state = &tstate->interp->ceval;

/* Check the region invariant if required. */
if (_Py_CheckRegionInvariant(tstate) != 0) {
return -1;
}

/* Pending signals */
if (_Py_atomic_load_relaxed_int32(&ceval->signals_pending)) {
if (handle_signals(tstate) != 0) {
Expand Down
2 changes: 2 additions & 0 deletions Python/ceval_macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
/* Do interpreter dispatch accounting for tracing and instrumentation */
#define DISPATCH() \
{ \
if (_Py_CheckRegionInvariant(tstate) != 0) \
goto error; \
NEXTOPARG(); \
PRE_DISPATCH_GOTO(); \
DISPATCH_GOTO(); \
Expand Down
58 changes: 56 additions & 2 deletions Python/clinic/bltinmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.