From 48ec0e16751e8c2846d7d97f49f681a900572016 Mon Sep 17 00:00:00 2001
From: David Malcolm <dmalcolm@redhat.com>
Date: Thu, 17 Sep 2020 06:34:44 -0400
Subject: [PATCH 45/47] FIXME: WIP on analyzer plugins (gil example)

---
 gcc/analyzer/analyzer.h                       |  11 +
 gcc/analyzer/engine.cc                        |  30 ++
 gcc/plugin.c                                  |   2 +
 gcc/plugin.def                                |   3 +
 .../gcc.dg/plugin/analyzer_gil_plugin.c       | 451 ++++++++++++++++++
 gcc/testsuite/gcc.dg/plugin/gil-1.c           |  76 +++
 gcc/testsuite/gcc.dg/plugin/plugin.exp        |   2 +
 7 files changed, 575 insertions(+)
 create mode 100644 gcc/testsuite/gcc.dg/plugin/analyzer_gil_plugin.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/gil-1.c

diff --git a/gcc/analyzer/analyzer.h b/gcc/analyzer/analyzer.h
index 17439175cc9..b24874ac2ae 100644
--- a/gcc/analyzer/analyzer.h
+++ b/gcc/analyzer/analyzer.h
@@ -98,6 +98,8 @@ class state_change;
 class rewind_info_t;
 
 class engine;
+class state_machine;
+class logger;
 
 /* Forward decls of functions.  */
 
@@ -187,6 +189,15 @@ private:
 
 extern location_t get_stmt_location (const gimple *stmt, function *fun);
 
+/* Passed by pointer to PLUGIN_ANALYZER_INIT callbacks.  */
+
+class plugin_analyzer_init_iface
+{
+public:
+  virtual void register_state_machine (state_machine *) = 0;
+  virtual logger *get_logger () const = 0;
+};
+
 } // namespace ana
 
 extern bool is_special_named_call_p (const gcall *call, const char *funcname,
diff --git a/gcc/analyzer/engine.cc b/gcc/analyzer/engine.cc
index ab109259921..21245425f07 100644
--- a/gcc/analyzer/engine.cc
+++ b/gcc/analyzer/engine.cc
@@ -65,6 +65,7 @@ along with GCC; see the file COPYING3.  If not see
 #include <zlib.h>
 #include "except.h"
 #include "tree-cfg.h"
+#include "plugin.h"
 
 /* For an overview, see gcc/doc/analyzer.texi.  */
 
@@ -5205,6 +5206,32 @@ dump_analyzer_json (const supergraph &sg,
   free (filename);
 }
 
+/* FIXME.  */
+
+class plugin_analyzer_init_impl : public plugin_analyzer_init_iface
+{
+public:
+  plugin_analyzer_init_impl (auto_delete_vec <state_machine> *checkers,
+			     logger *logger)
+  : m_checkers (checkers),
+    m_logger (logger)
+  {}
+
+  void register_state_machine (state_machine *sm) FINAL OVERRIDE
+  {
+    m_checkers->safe_push (sm);
+  }
+
+  logger *get_logger () const FINAL OVERRIDE
+  {
+    return m_logger;
+  }
+
+private:
+  auto_delete_vec <state_machine> *m_checkers;
+  logger *m_logger;
+};
+
 /* Run the analysis "engine".  */
 
 void
@@ -5250,6 +5277,9 @@ impl_run_checkers (logger *logger)
   auto_delete_vec <state_machine> checkers;
   make_checkers (checkers, logger);
 
+  plugin_analyzer_init_impl data (&checkers, logger);
+  invoke_plugin_callbacks (PLUGIN_ANALYZER_INIT, &data);
+
   if (logger)
     {
       int i;
diff --git a/gcc/plugin.c b/gcc/plugin.c
index 69b6f5b8b6a..76069e64798 100644
--- a/gcc/plugin.c
+++ b/gcc/plugin.c
@@ -497,6 +497,7 @@ register_callback (const char *plugin_name,
       case PLUGIN_EARLY_GIMPLE_PASSES_END:
       case PLUGIN_NEW_PASS:
       case PLUGIN_INCLUDE_FILE:
+      case PLUGIN_ANALYZER_INIT:
         {
           struct callback_info *new_callback;
           if (!callback)
@@ -577,6 +578,7 @@ invoke_plugin_callbacks_full (int event, void *gcc_data)
       case PLUGIN_EARLY_GIMPLE_PASSES_END:
       case PLUGIN_NEW_PASS:
       case PLUGIN_INCLUDE_FILE:
+      case PLUGIN_ANALYZER_INIT:
         {
           /* Iterate over every callback registered with this event and
              call it.  */
diff --git a/gcc/plugin.def b/gcc/plugin.def
index 3482ed71906..0c4a19739f6 100644
--- a/gcc/plugin.def
+++ b/gcc/plugin.def
@@ -99,6 +99,9 @@ DEFEVENT (PLUGIN_NEW_PASS)
    as a const char* pointer.  */
 DEFEVENT (PLUGIN_INCLUDE_FILE)
 
+/* Called when -fanalyzer starts.  */
+DEFEVENT (PLUGIN_ANALYZER_INIT)
+
 /* When adding a new hard-coded plugin event, don't forget to edit in
    file plugin.c the functions register_callback and
    invoke_plugin_callbacks_full accordingly!  */
diff --git a/gcc/testsuite/gcc.dg/plugin/analyzer_gil_plugin.c b/gcc/testsuite/gcc.dg/plugin/analyzer_gil_plugin.c
new file mode 100644
index 00000000000..616bb30cfbd
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/analyzer_gil_plugin.c
@@ -0,0 +1,451 @@
+/* Proof-of-concept of a -fanalyzer plugin.
+   Detect (some) uses of CPython API outside of the Global Interpreter Lock.
+   https://docs.python.org/3/c-api/init.html#thread-state-and-the-global-interpreter-lock
+*/
+/* { dg-options "-g" } */
+
+#include "gcc-plugin.h"
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "diagnostic.h"
+#include "edit-context.h"
+#include "selftest.h"
+#include "selftest-diagnostic.h"
+#include "tree.h"
+#include "analyzer/analyzer.h"
+#include "analyzer/analyzer-logging.h"
+
+// FIXME: prune this:
+#include "tree.h"
+#include "function.h"
+#include "basic-block.h"
+#include "gimple.h"
+#include "options.h"
+#include "bitmap.h"
+#include "diagnostic-path.h"
+#include "diagnostic-metadata.h"
+#include "function.h"
+#include "analyzer/analyzer.h"
+#include "diagnostic-event-id.h"
+#include "analyzer/analyzer-logging.h"
+#include "analyzer/sm.h"
+#include "analyzer/pending-diagnostic.h"
+#include "sbitmap.h"
+#include "tristate.h"
+#include "ordered-hash-map.h"
+#include "selftest.h"
+#include "analyzer/call-string.h"
+#include "analyzer/program-point.h"
+#include "analyzer/store.h"
+#include "analyzer/region-model.h"
+#include "analyzer/program-state.h"
+#include "analyzer/checker-path.h"
+#include "digraph.h"
+#include "cfg.h"
+#include "gimple-iterator.h"
+#include "cgraph.h"
+#include "analyzer/supergraph.h"
+#include "alloc-pool.h"
+#include "fibonacci_heap.h"
+#include "analyzer/diagnostic-manager.h"
+#include "shortest-paths.h"
+#include "analyzer/exploded-graph.h"
+#include "analyzer/function-set.h"
+#include "analyzer/analyzer-selftests.h"
+#include "gimple-walk.h"
+
+int plugin_is_GPL_compatible;
+
+#if ENABLE_ANALYZER
+
+namespace ana {
+
+// FIXME: was a copy-and-paste from sm-signal.cc
+
+/* An experimental state machine, for tracking calls to async-signal-unsafe
+   functions from within signal handlers.  */
+
+class gil_state_machine : public state_machine
+{
+public:
+  gil_state_machine (logger *logger);
+
+  bool inherited_state_p () const FINAL OVERRIDE { return false; }
+
+  bool on_stmt (sm_context *sm_ctxt,
+		const supernode *node,
+		const gimple *stmt) const FINAL OVERRIDE;
+
+  void on_condition (sm_context *sm_ctxt,
+		     const supernode *node,
+		     const gimple *stmt,
+		     tree lhs,
+		     enum tree_code op,
+		     tree rhs) const FINAL OVERRIDE;
+
+  bool can_purge_p (state_t s) const FINAL OVERRIDE;
+
+  void check_for_pyobject_usage_without_gil (sm_context *sm_ctxt,
+					     const supernode *node,
+					     const gimple *stmt,
+					     tree op) const;
+
+  /* These states are "global", rather than per-expression.  */
+
+  /* State for when we've released the GIL.  */
+  state_t m_released_gil;
+
+  /* Stop state.  */
+  state_t m_stop;
+};
+
+// FIXME
+/* Concrete subclass for describing call to an async-signal-unsafe function
+   from a signal handler.  */
+
+class gil_diagnostic : public pending_diagnostic
+{
+public:
+  label_text describe_state_change (const evdesc::state_change &change)
+    FINAL OVERRIDE
+  {
+    if (change.is_global_p ()
+	&& change.m_new_state == m_sm.m_released_gil)
+      return change.formatted_print ("releasing the GIL here");
+    if (change.is_global_p ()
+	&& change.m_new_state == m_sm.get_start_state ())
+      return change.formatted_print ("acquiring the GIL here");
+    return label_text ();
+  }
+
+ protected:
+  gil_diagnostic (const gil_state_machine &sm) : m_sm (sm)
+  {
+  }
+
+ private:
+  const gil_state_machine &m_sm;
+};
+
+class double_save_thread : public gil_diagnostic
+{
+ public:
+  double_save_thread (const gil_state_machine &sm, const gcall *call)
+  : gil_diagnostic (sm), m_call (call)
+  {}
+
+  const char *get_kind () const FINAL OVERRIDE
+  {
+    return "double_save_thread";
+  }
+
+  bool subclass_equal_p (const pending_diagnostic &base_other) const OVERRIDE
+  {
+    return true; // TODO
+  }
+
+  bool emit (rich_location *rich_loc) FINAL OVERRIDE
+  {
+    // TODO
+    auto_diagnostic_group d;
+    return warning_at (rich_loc, 0, // FIXME
+		       "FIXME: double save thread");
+  }
+
+ private:
+  const gcall *m_call;
+};
+
+class double_restore_thread : public gil_diagnostic
+{
+ public:
+  double_restore_thread (const gil_state_machine &sm, const gcall *call)
+  : gil_diagnostic (sm), m_call (call)
+  {}
+
+  const char *get_kind () const FINAL OVERRIDE
+  {
+    return "double_restore_thread";
+  }
+
+  bool subclass_equal_p (const pending_diagnostic &base_other) const OVERRIDE
+  {
+    return true; // TODO
+  }
+
+  bool emit (rich_location *rich_loc) FINAL OVERRIDE
+  {
+    // TODO
+    auto_diagnostic_group d;
+    return warning_at (rich_loc, 0, // FIXME
+		       "FIXME: double restore thread");
+  }
+
+ private:
+  const gcall *m_call;
+};
+
+class fncall_without_gil : public gil_diagnostic
+{
+ public:
+  fncall_without_gil (const gil_state_machine &sm, const gcall *call)
+  : gil_diagnostic (sm), m_call (call)
+  {}
+
+  const char *get_kind () const FINAL OVERRIDE
+  {
+    return "fncall_without_gil";
+  }
+
+  bool subclass_equal_p (const pending_diagnostic &base_other) const OVERRIDE
+  {
+    return true; // TODO
+  }
+
+  bool emit (rich_location *rich_loc) FINAL OVERRIDE
+  {
+    // TODO
+    auto_diagnostic_group d;
+    return warning_at (rich_loc, 0, // FIXME
+		       "FIXME: fncall_without_gil");
+  }
+
+ private:
+  const gcall *m_call;
+};
+
+class pyobject_usage_without_gil : public gil_diagnostic
+{
+ public:
+  pyobject_usage_without_gil (const gil_state_machine &sm)
+  : gil_diagnostic (sm)
+  {}
+
+  const char *get_kind () const FINAL OVERRIDE
+  {
+    return "pyobject_usage_without_gil";
+  }
+
+  bool subclass_equal_p (const pending_diagnostic &base_other) const OVERRIDE
+  {
+    return true; // TODO
+  }
+
+  bool emit (rich_location *rich_loc) FINAL OVERRIDE
+  {
+    // TODO
+    auto_diagnostic_group d;
+    return warning_at (rich_loc, 0, // FIXME
+		       "FIXME: pyobject_usage_without_gil");
+  }
+
+ private:
+};
+
+/* gil_state_machine's ctor.  */
+
+gil_state_machine::gil_state_machine (logger *logger)
+: state_machine ("gil", logger)
+{
+  m_released_gil = add_state ("released_gil");
+  m_stop = add_state ("stop");
+}
+
+/* Get a set of functions that are known to require the GIL.  */
+
+static function_set
+get_fns_requiring_gil ()
+{
+  // TODO: populate this list more fully
+  static const char * const fns_requiring_gil[] = {
+    /* This array must be kept sorted.  */
+    "_Py_DECREF",
+    "_Py_INCREF"
+  };
+  const size_t count
+    = sizeof(fns_requiring_gil) / sizeof (fns_requiring_gil[0]);
+  function_set fs (fns_requiring_gil, count);
+  return fs;
+}
+
+/* Return true if FNDECL is known to require the GIL.  */
+
+static bool
+requires_gil_p (tree fndecl)
+{
+  function_set fs = get_fns_requiring_gil ();
+  return fs.contains_decl_p (fndecl);
+}
+
+struct cb_data
+{
+  cb_data (const gil_state_machine &sm, sm_context *sm_ctxt,
+	   const supernode *snode, const gimple *stmt)
+  : m_sm (sm), m_sm_ctxt (sm_ctxt), m_snode (snode), m_stmt (stmt)
+  {
+  }
+
+  const gil_state_machine &m_sm;
+  sm_context *m_sm_ctxt;
+  const supernode *m_snode;
+  const gimple *m_stmt;
+};
+
+static bool
+check_for_pyobject (gimple *, tree op, tree, void *data)
+{
+  cb_data *d = (cb_data *)data;
+  d->m_sm.check_for_pyobject_usage_without_gil (d->m_sm_ctxt, d->m_snode,
+						d->m_stmt, op);
+  return true;
+}
+
+/* Implementation of state_machine::on_stmt vfunc for gil_state_machine.  */
+
+bool
+gil_state_machine::on_stmt (sm_context *sm_ctxt,
+			    const supernode *node,
+			    const gimple *stmt) const
+{
+  const state_t global_state = sm_ctxt->get_global_state ();
+  if (const gcall *call = dyn_cast <const gcall *> (stmt))
+    {
+      if (tree callee_fndecl = sm_ctxt->get_fndecl_for_call (call))
+	{
+	  if (is_named_call_p (callee_fndecl, "PyEval_SaveThread", call, 0))
+	    {
+	      if (0)
+		inform (input_location, "found call to %qs",
+			"PyEval_SaveThread");
+	      if (global_state == m_released_gil)
+		{
+		  sm_ctxt->warn (node, stmt, NULL_TREE,
+				 new double_save_thread (*this, call));
+		  sm_ctxt->set_global_state (m_stop);
+		}
+	      else
+		sm_ctxt->set_global_state (m_released_gil);
+	      return true;
+	    }
+	  else if (is_named_call_p (callee_fndecl, "PyEval_RestoreThread",
+				    call, 1))
+	    {
+	      if (0)
+		inform (input_location, "found call to %qs",
+			"PyEval_SaveThread");
+	      if (global_state == m_released_gil)
+		sm_ctxt->set_global_state (m_start);
+	      else
+		{
+		  sm_ctxt->warn (node, stmt, NULL_TREE,
+				 new double_restore_thread (*this, call));
+		  sm_ctxt->set_global_state (m_stop);
+		}
+	      return true;
+	    }
+	  else if (global_state == m_released_gil)
+	    {
+	      /* TODO: check for use of fns that require the GIL.  */
+	      if (requires_gil_p (callee_fndecl))
+		{
+		  sm_ctxt->warn (node, stmt, NULL_TREE,
+				 new fncall_without_gil (*this, call));
+		  sm_ctxt->set_global_state (m_stop);
+		}
+	    }
+	}
+    }
+  else
+    if (global_state == m_released_gil)
+      {
+	// TODO: walk expr, finding uses of PyObject (or "subclasses")
+	cb_data d (*this, sm_ctxt, node, stmt);
+	walk_stmt_load_store_addr_ops (const_cast <gimple *> (stmt), &d,
+				       check_for_pyobject,
+				       check_for_pyobject,
+				       check_for_pyobject);
+    }
+  return false;
+}
+
+/* Implementation of state_machine::on_condition vfunc for
+   gil_state_machine.  */
+
+void
+gil_state_machine::on_condition (sm_context *sm_ctxt ATTRIBUTE_UNUSED,
+				 const supernode *node ATTRIBUTE_UNUSED,
+				 const gimple *stmt ATTRIBUTE_UNUSED,
+				 tree lhs ATTRIBUTE_UNUSED,
+				 enum tree_code op ATTRIBUTE_UNUSED,
+				 tree rhs ATTRIBUTE_UNUSED) const
+{
+  // Empty
+}
+
+bool
+gil_state_machine::can_purge_p (state_t s ATTRIBUTE_UNUSED) const
+{
+  return true;
+}
+
+void
+gil_state_machine::check_for_pyobject_usage_without_gil (sm_context *sm_ctxt,
+							 const supernode *node,
+							 const gimple *stmt,
+							 tree op) const
+{
+  tree type = TREE_TYPE (op);
+  if (TREE_CODE (type) == RECORD_TYPE)
+    //&& FIXME ("PyObject"))
+    // FIXME: "subclasses"
+    {
+      sm_ctxt->warn (node, stmt, NULL_TREE,
+		     new pyobject_usage_without_gil (*this));
+      sm_ctxt->set_global_state (m_stop);
+    }
+  //inform (input_location, "got usage of PyObject"); // TODO
+}
+
+/* Callback handler for the PLUGIN_ANALYZER_INIT event.  */
+
+static void
+gil_analyzer_init_cb (void *gcc_data, void */*user_data*/)
+{
+  ana::plugin_analyzer_init_iface *iface
+    = (ana::plugin_analyzer_init_iface *)gcc_data;
+  LOG_SCOPE (iface->get_logger ());
+  if (0)
+    inform (input_location, "got here: gil_analyzer_init_cb");
+  iface->register_state_machine (new gil_state_machine (iface->get_logger ()));
+}
+
+} // namespace ana
+
+#endif /* #if ENABLE_ANALYZER */
+
+int
+plugin_init (struct plugin_name_args *plugin_info,
+	     struct plugin_gcc_version *version)
+{
+#if ENABLE_ANALYZER
+  const char *plugin_name = plugin_info->base_name;
+  if (0)
+    inform (input_location, "got here; %qs", plugin_name);
+  register_callback (plugin_info->base_name,
+		     PLUGIN_ANALYZER_INIT,
+		     ana::gil_analyzer_init_cb,
+		     NULL); /* void *user_data */
+#else
+  /* FIXME: move this into a helper function (to be shared with
+     pass_analyzer::execute), and maybe move it to inside
+     register_callback?  */
+  sorry ("%qs was not enabled in this build of GCC"
+	 " (missing configure-time option %qs)",
+	 "-fanalyzer", "--enable-analyzer");
+#endif /* #if ENABLE_ANALYZER */
+  return 0;
+}
+
+/* TODO:
+   - document PLUGIN_ANALYZER_INIT.  */
diff --git a/gcc/testsuite/gcc.dg/plugin/gil-1.c b/gcc/testsuite/gcc.dg/plugin/gil-1.c
new file mode 100644
index 00000000000..0d25a612432
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/gil-1.c
@@ -0,0 +1,76 @@
+/* { dg-do compile } */
+/* { dg-options "-fanalyzer" } */
+
+/* TODO.  */
+
+#if 0
+Py_BEGIN_ALLOW_THREADS
+... Do some blocking I/O operation ...
+Py_END_ALLOW_THREADS
+The Py_BEGIN_ALLOW_THREADS macro opens a new block and declares a hidden local variable; the Py_END_ALLOW_THREADS macro closes the block.
+
+The block above expands to the following code:
+
+PyThreadState *_save;
+
+_save = PyEval_SaveThread();
+... Do some blocking I/O operation ...
+PyEval_RestoreThread(_save);
+#endif
+
+/* Adapted from CPython 3.8's ceval.h.  */
+typedef struct PyThreadState PyThreadState;
+extern PyThreadState * PyEval_SaveThread(void);
+extern void PyEval_RestoreThread(PyThreadState *);
+
+#define Py_BEGIN_ALLOW_THREADS { \
+                        PyThreadState *_save; \
+                        _save = PyEval_SaveThread();
+#define Py_BLOCK_THREADS        PyEval_RestoreThread(_save);
+#define Py_UNBLOCK_THREADS      _save = PyEval_SaveThread();
+#define Py_END_ALLOW_THREADS    PyEval_RestoreThread(_save); \
+                 }
+
+/* Adapted/hacked up from CPython 3.8's object.h.  */
+
+typedef struct _object {
+    int ob_refcnt;
+} PyObject;
+
+static inline void Py_INCREF(PyObject *op)
+{
+  op->ob_refcnt++;
+}
+static inline void Py_DECREF(PyObject *op)
+{
+  op->ob_refcnt--;
+}
+
+void test_1 (void)
+{
+  Py_BEGIN_ALLOW_THREADS
+  Py_END_ALLOW_THREADS
+}
+
+void test_2 (PyObject *obj)
+{
+  Py_BEGIN_ALLOW_THREADS
+
+  Py_INCREF (obj); // FIXME!
+
+  Py_END_ALLOW_THREADS
+}
+
+/* TODO: shouldn't be nested.  */
+/* FIXME:
+   WARNING: NEVER NEST CALLS TO Py_BEGIN_ALLOW_THREADS AND
+   Py_END_ALLOW_THREADS!!!
+*/
+
+void test_3 (PyObject *obj)
+{
+  Py_BEGIN_ALLOW_THREADS
+  Py_BEGIN_ALLOW_THREADS
+  Py_END_ALLOW_THREADS
+  Py_END_ALLOW_THREADS
+}
diff --git a/gcc/testsuite/gcc.dg/plugin/plugin.exp b/gcc/testsuite/gcc.dg/plugin/plugin.exp
index c02b0082715..ba71b84240b 100644
--- a/gcc/testsuite/gcc.dg/plugin/plugin.exp
+++ b/gcc/testsuite/gcc.dg/plugin/plugin.exp
@@ -118,6 +118,8 @@ set plugin_test_list [list \
     { dump_plugin.c \
 	  dump-1.c \
 	  dump-2.c } \
+    { analyzer_gil_plugin.c \
+	  gil-1.c } \
 ]
 
 foreach plugin_test $plugin_test_list {
-- 
2.26.2

