aboutsummaryrefslogtreecommitdiff
path: root/gcc/analyzer/infinite-recursion.cc
diff options
context:
space:
mode:
Diffstat (limited to 'gcc/analyzer/infinite-recursion.cc')
-rw-r--r--gcc/analyzer/infinite-recursion.cc481
1 files changed, 481 insertions, 0 deletions
diff --git a/gcc/analyzer/infinite-recursion.cc b/gcc/analyzer/infinite-recursion.cc
new file mode 100644
index 0000000..7055926
--- /dev/null
+++ b/gcc/analyzer/infinite-recursion.cc
@@ -0,0 +1,481 @@
+/* Detection of infinite recursion.
+ Copyright (C) 2022 Free Software Foundation, Inc.
+ Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3, or (at your option)
+any later version.
+
+GCC is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+<http://www.gnu.org/licenses/>. */
+
+#include "config.h"
+#define INCLUDE_MEMORY
+#include "system.h"
+#include "coretypes.h"
+#include "tree.h"
+#include "fold-const.h"
+#include "gcc-rich-location.h"
+#include "alloc-pool.h"
+#include "fibonacci_heap.h"
+#include "shortest-paths.h"
+#include "diagnostic-core.h"
+#include "diagnostic-event-id.h"
+#include "diagnostic-path.h"
+#include "diagnostic-metadata.h"
+#include "function.h"
+#include "pretty-print.h"
+#include "sbitmap.h"
+#include "bitmap.h"
+#include "tristate.h"
+#include "ordered-hash-map.h"
+#include "selftest.h"
+#include "json.h"
+#include "analyzer/analyzer.h"
+#include "analyzer/analyzer-logging.h"
+#include "analyzer/call-string.h"
+#include "analyzer/program-point.h"
+#include "analyzer/store.h"
+#include "analyzer/region-model.h"
+#include "analyzer/constraint-manager.h"
+#include "analyzer/sm.h"
+#include "analyzer/pending-diagnostic.h"
+#include "analyzer/diagnostic-manager.h"
+#include "cfg.h"
+#include "basic-block.h"
+#include "gimple.h"
+#include "gimple-iterator.h"
+#include "gimple-pretty-print.h"
+#include "cgraph.h"
+#include "digraph.h"
+#include "analyzer/supergraph.h"
+#include "analyzer/program-state.h"
+#include "analyzer/exploded-graph.h"
+#include "make-unique.h"
+#include "analyzer/checker-path.h"
+
+/* A subclass of pending_diagnostic for complaining about suspected
+ infinite recursion. */
+
+class infinite_recursion_diagnostic
+: public pending_diagnostic_subclass<infinite_recursion_diagnostic>
+{
+public:
+ infinite_recursion_diagnostic (const exploded_node *prev_entry_enode,
+ const exploded_node *new_entry_enode,
+ tree callee_fndecl)
+ : m_prev_entry_enode (prev_entry_enode),
+ m_new_entry_enode (new_entry_enode),
+ m_callee_fndecl (callee_fndecl),
+ m_prev_entry_event (NULL)
+ {}
+
+ const char *get_kind () const final override
+ {
+ return "infinite_recursion_diagnostic";
+ }
+
+ bool operator== (const infinite_recursion_diagnostic &other) const
+ {
+ return m_callee_fndecl == other.m_callee_fndecl;
+ }
+
+ int get_controlling_option () const final override
+ {
+ return OPT_Wanalyzer_infinite_recursion;
+ }
+
+ bool emit (rich_location *rich_loc) final override
+ {
+ /* "CWE-674: Uncontrolled Recursion". */
+ diagnostic_metadata m;
+ m.add_cwe (674);
+ return warning_meta (rich_loc, m, get_controlling_option (),
+ "infinite recursion");
+ }
+
+ label_text describe_final_event (const evdesc::final_event &ev) final override
+ {
+ const int frames_consumed = (m_new_entry_enode->get_stack_depth ()
+ - m_prev_entry_enode->get_stack_depth ());
+ if (frames_consumed > 1)
+ return ev.formatted_print
+ ("apparently infinite chain of mutually-recursive function calls,"
+ " consuming %i stack frames per recursion",
+ frames_consumed);
+ else
+ return ev.formatted_print ("apparently infinite recursion");
+ }
+
+ void
+ add_function_entry_event (const exploded_edge &eedge,
+ checker_path *emission_path) final override
+ {
+ /* Subclass of function_entry_event for use when reporting both
+ the initial and subsequent entries to the function of interest,
+ allowing for cross-referencing the first event in the description
+ of the second. */
+ class recursive_function_entry_event : public function_entry_event
+ {
+ public:
+ recursive_function_entry_event (const program_point &dst_point,
+ const infinite_recursion_diagnostic &pd,
+ bool topmost)
+ : function_entry_event (dst_point),
+ m_pd (pd),
+ m_topmost (topmost)
+ {
+ }
+
+ label_text
+ get_desc (bool can_colorize) const final override
+ {
+ if (m_topmost)
+ {
+ if (m_pd.m_prev_entry_event
+ && m_pd.m_prev_entry_event->get_id_ptr ()->known_p ())
+ return make_label_text
+ (can_colorize,
+ "recursive entry to %qE; previously entered at %@",
+ m_effective_fndecl,
+ m_pd.m_prev_entry_event->get_id_ptr ());
+ else
+ return make_label_text (can_colorize, "recursive entry to %qE",
+ m_effective_fndecl);
+ }
+ else
+ return make_label_text (can_colorize, "initial entry to %qE",
+ m_effective_fndecl);
+ }
+
+ private:
+ const infinite_recursion_diagnostic &m_pd;
+ bool m_topmost;
+ };
+ const exploded_node *dst_node = eedge.m_dest;
+ const program_point &dst_point = dst_node->get_point ();
+ if (eedge.m_dest == m_prev_entry_enode)
+ {
+ gcc_assert (m_prev_entry_event == NULL);
+ std::unique_ptr<checker_event> prev_entry_event
+ = make_unique <recursive_function_entry_event> (dst_point,
+ *this, false);
+ m_prev_entry_event = prev_entry_event.get ();
+ emission_path->add_event (std::move (prev_entry_event));
+ }
+ else if (eedge.m_dest == m_new_entry_enode)
+ emission_path->add_event
+ (make_unique<recursive_function_entry_event> (dst_point, *this, true));
+ else
+ pending_diagnostic::add_function_entry_event (eedge, emission_path);
+ }
+
+ /* Customize the location where the warning_event appears, putting
+ it at the topmost entrypoint to the function. */
+ void add_final_event (const state_machine *,
+ const exploded_node *,
+ const gimple *,
+ tree,
+ state_machine::state_t,
+ checker_path *emission_path) final override
+ {
+ gcc_assert (m_new_entry_enode);
+ emission_path->add_event
+ (make_unique<warning_event>
+ (m_new_entry_enode->get_supernode ()->get_start_location (),
+ m_callee_fndecl,
+ m_new_entry_enode->get_stack_depth (),
+ NULL, NULL, NULL));
+ }
+
+private:
+ const exploded_node *m_prev_entry_enode;
+ const exploded_node *m_new_entry_enode;
+ tree m_callee_fndecl;
+ const checker_event *m_prev_entry_event;
+};
+
+/* Return true iff ENODE is the PK_BEFORE_SUPERNODE at a function
+ entrypoint. */
+
+static bool
+is_entrypoint_p (exploded_node *enode)
+{
+ /* Look for an entrypoint to a function... */
+ const supernode *snode = enode->get_supernode ();
+ if (!snode)
+ return false;
+ if (!snode->entry_p ())
+ return false;;
+ const program_point &point = enode->get_point ();
+ if (point.get_kind () != PK_BEFORE_SUPERNODE)
+ return false;
+ return true;
+}
+
+/* Walk backwards through the eg, looking for the first
+ enode we find that's also the entrypoint of the same function. */
+
+exploded_node *
+exploded_graph::find_previous_entry_to (function *top_of_stack_fun,
+ exploded_node *enode) const
+{
+ auto_vec<exploded_node *> worklist;
+ hash_set<exploded_node *> visited;
+
+ visited.add (enode);
+ for (auto in_edge : enode->m_preds)
+ worklist.safe_push (in_edge->m_src);
+
+ while (worklist.length () > 0)
+ {
+ exploded_node *iter = worklist.pop ();
+
+ if (is_entrypoint_p (iter)
+ && iter->get_function () == top_of_stack_fun)
+ return iter;
+
+ if (visited.contains (iter))
+ continue;
+ visited.add (iter);
+ for (auto in_edge : iter->m_preds)
+ worklist.safe_push (in_edge->m_src);
+ }
+
+ /* Not found. */
+ return NULL;
+}
+
+/* Given BASE_REG within ENCLOSING_FRAME (such as a function parameter),
+ remap it to the equivalent region within EQUIV_PREV_FRAME.
+
+ For example, given param "n" within frame "foo@3", and equiv prev frame
+ "foo@1", remap it to param "n" within frame "foo@1". */
+
+static const region *
+remap_enclosing_frame (const region *base_reg,
+ const frame_region *enclosing_frame,
+ const frame_region *equiv_prev_frame,
+ region_model_manager *mgr)
+{
+ gcc_assert (base_reg->get_parent_region () == enclosing_frame);
+ switch (base_reg->get_kind ())
+ {
+ default:
+ /* We should only encounter params and varargs at the topmost
+ entrypoint. */
+ gcc_unreachable ();
+
+ case RK_VAR_ARG:
+ {
+ const var_arg_region *var_arg_reg = (const var_arg_region *)base_reg;
+ return mgr->get_var_arg_region (equiv_prev_frame,
+ var_arg_reg->get_index ());
+ }
+ case RK_DECL:
+ {
+ const decl_region *decl_reg = (const decl_region *)base_reg;
+ return equiv_prev_frame->get_region_for_local (mgr,
+ decl_reg->get_decl (),
+ NULL);
+ }
+ }
+}
+
+/* Compare the state of memory at NEW_ENTRY_ENODE and PREV_ENTRY_ENODE,
+ both of which are entrypoints to the same function, where recursion has
+ occurred.
+
+ Return true if the state of NEW_ENTRY_ENODE is sufficiently different
+ from PREV_ENTRY_ENODE to suggests that some variant is being modified,
+ and thus the recursion isn't infinite.
+
+ Return false if the states are effectively the same, suggesting that
+ the recursion is infinite.
+
+ For example, consider mutually recursive functions "foo" and "bar".
+ At the entrypoint to a "foo" frame where we've detected recursion,
+ we might have three frames on the stack: the new 'foo'@3, an inner
+ 'bar'@2, and the innermost 'foo'@1.
+
+ (gdb) call enode->dump(m_ext_state)
+ EN: 16
+ callstring: [(SN: 9 -> SN: 3 in foo), (SN: 5 -> SN: 8 in bar)]
+ before SN: 0 (NULL from-edge)
+
+ rmodel:
+ stack depth: 3
+ frame (index 2): frame: ‘foo’@3
+ frame (index 1): frame: ‘bar’@2
+ frame (index 0): frame: ‘foo’@1
+ clusters within root region
+ cluster for: (*INIT_VAL(f_4(D)))
+ clusters within frame: ‘bar’@2
+ cluster for: b_2(D): INIT_VAL(f_4(D))
+ clusters within frame: ‘foo’@3
+ cluster for: f_4(D): INIT_VAL(f_4(D))
+ m_called_unknown_fn: FALSE
+
+ whereas for the previous entry node we'd have just the innermost
+ 'foo'@1
+
+ (gdb) call prev_entry_enode->dump(m_ext_state)
+ EN: 1
+ callstring: []
+ before SN: 0 (NULL from-edge)
+
+ rmodel:
+ stack depth: 1
+ frame (index 0): frame: ‘foo’@1
+ clusters within root region
+ cluster for: (*INIT_VAL(f_4(D)))
+ m_called_unknown_fn: FALSE
+
+ We want to abstract away frames 1 and 2 in the new entry enode,
+ and compare its frame 3 with the frame 1 in the previous entry
+ enode, and determine if enough state changes between them to
+ rule out infinite recursion. */
+
+static bool
+sufficiently_different_p (exploded_node *new_entry_enode,
+ exploded_node *prev_entry_enode,
+ logger *logger)
+{
+ LOG_SCOPE (logger);
+ gcc_assert (new_entry_enode);
+ gcc_assert (prev_entry_enode);
+ gcc_assert (is_entrypoint_p (new_entry_enode));
+ gcc_assert (is_entrypoint_p (prev_entry_enode));
+
+ const int new_stack_depth = new_entry_enode->get_stack_depth ();
+
+ /* Compare the stores of the two enodes. */
+ const region_model &new_model
+ = *new_entry_enode->get_state ().m_region_model;
+ const region_model &prev_model
+ = *prev_entry_enode->get_state ().m_region_model;
+ const store &new_store = *new_model.get_store ();
+
+ for (auto kv : new_store)
+ {
+ const region *base_reg = kv.first;
+
+ /* Get the value within the new frame. */
+ const svalue *new_sval
+ = new_model.get_store_value (base_reg, NULL);
+
+ /* If the value is UNKNOWN (e.g. due to hitting complexity limits)
+ assume that it differs from the previous value. */
+ if (new_sval->get_kind () == SK_UNKNOWN)
+ return true;
+
+ /* Get the equivalent value within the old enode. */
+ const svalue *prev_sval;
+
+ if (const frame_region *enclosing_frame
+ = base_reg->maybe_get_frame_region ())
+ {
+ /* We have a binding within a frame in the new entry enode. */
+
+ /* Ignore bindings within frames below the new entry node. */
+ if (enclosing_frame->get_stack_depth () < new_stack_depth)
+ continue;
+
+ /* We have a binding within the frame of the new entry node,
+ presumably a parameter. */
+
+ /* Get the value within the equivalent frame of
+ the old entrypoint; typically will be the initial_svalue
+ of the parameter. */
+ const frame_region *equiv_prev_frame
+ = prev_model.get_current_frame ();
+ const region *equiv_prev_base_reg
+ = remap_enclosing_frame (base_reg,
+ enclosing_frame,
+ equiv_prev_frame,
+ new_model.get_manager ());
+ prev_sval = prev_model.get_store_value (equiv_prev_base_reg, NULL);
+ }
+ else
+ prev_sval = prev_model.get_store_value (base_reg, NULL);
+
+ /* If the prev_sval is UNKNOWN (e.g. due to hitting complexity limits)
+ assume that it will differ from any new value. */
+ if (prev_sval->get_kind () == SK_UNKNOWN)
+ return true;
+
+ if (new_sval != prev_sval)
+ return true;
+ }
+
+ /* No significant differences found. */
+ return false;
+}
+
+/* Implementation of -Wanalyzer-infinite-recursion.
+
+ Called when adding ENODE to the graph, after adding its first in-edge.
+
+ For function entrypoints, see if recursion has occurred, and, if so,
+ check if the state of memory changed between the recursion levels,
+ which would suggest some kind of decreasing variant that leads to
+ termination.
+
+ For recursive calls where the state of memory is effectively unchanged
+ between recursion levels, warn with -Wanalyzer-infinite-recursion. */
+
+void
+exploded_graph::detect_infinite_recursion (exploded_node *enode)
+{
+ if (!is_entrypoint_p (enode))
+ return;
+ function *top_of_stack_fun = enode->get_function ();
+ gcc_assert (top_of_stack_fun);
+
+ /* ....where a call to that function is already in the call string. */
+ const call_string &call_string = enode->get_point ().get_call_string ();
+
+ if (call_string.count_occurrences_of_function (top_of_stack_fun) < 2)
+ return;
+
+ tree fndecl = top_of_stack_fun->decl;
+
+ log_scope s (get_logger (),
+ "checking for infinite recursion",
+ "considering recursion at EN: %i entering %qE",
+ enode->m_index, fndecl);
+
+ /* Find enode that's the entrypoint for the previous frame for fndecl
+ in the recursion. */
+ exploded_node *prev_entry_enode
+ = find_previous_entry_to (top_of_stack_fun, enode);
+ gcc_assert (prev_entry_enode);
+ if (get_logger ())
+ get_logger ()->log ("previous entrypoint to %qE is EN: %i",
+ fndecl, prev_entry_enode->m_index);
+
+ /* Look for changes to the state of memory between the recursion levels. */
+ if (sufficiently_different_p (enode, prev_entry_enode, get_logger ()))
+ return;
+
+ /* Otherwise, the state of memory is effectively the same between the two
+ recursion levels; warn. */
+
+ const supernode *caller_snode = call_string.get_top_of_stack ().m_caller;
+ const supernode *snode = enode->get_supernode ();
+ gcc_assert (caller_snode->m_returning_call);
+ get_diagnostic_manager ().add_diagnostic
+ (enode, snode, caller_snode->m_returning_call, NULL,
+ make_unique<infinite_recursion_diagnostic> (prev_entry_enode,
+ enode,
+ fndecl));
+}