aboutsummaryrefslogtreecommitdiff
path: root/gcc/analyzer/sm-malloc.cc
diff options
context:
space:
mode:
authorDavid Malcolm <dmalcolm@redhat.com>2022-11-10 13:23:56 -0500
committerDavid Malcolm <dmalcolm@redhat.com>2022-11-10 13:23:56 -0500
commit5c6546ca7d8cab1f1c129f5b55f709e2ceee0f94 (patch)
tree4fac12e29958d8edf7753513459258980c8fe0b7 /gcc/analyzer/sm-malloc.cc
parent7e3ce73849fef8b50efb427ec96f317e88c0e6cf (diff)
downloadgcc-5c6546ca7d8cab1f1c129f5b55f709e2ceee0f94.zip
gcc-5c6546ca7d8cab1f1c129f5b55f709e2ceee0f94.tar.gz
gcc-5c6546ca7d8cab1f1c129f5b55f709e2ceee0f94.tar.bz2
analyzer: new warning: -Wanalyzer-deref-before-check [PR99671]
This patch implements a new -Wanalyzer-deref-before-check within -fanalyzer. It complains about code paths in which a pointer is checked for NULL after it has already been dereferenced. For example, for the testcase in PR 77432 the diagnostic emits: deref-before-check-1.c: In function 'test_from_pr77432': deref-before-check-1.c:6:8: warning: check of 'a' for NULL after already dereferencing it [-Wanalyzer-deref-before-check] 6 | if (a) | ^ 'test_from_pr77432': events 1-2 | | 5 | int b = *a; | | ^ | | | | | (1) pointer 'a' is dereferenced here | 6 | if (a) | | ~ | | | | | (2) pointer 'a' is checked for NULL here but it was already dereferenced at (1) | and in PR 77425 we had an instance of this hidden behind a macro, which the diagnostic complains about as follows: deref-before-check-pr77425.c: In function 'get_odr_type': deref-before-check-pr77425.c:35:10: warning: check of 'odr_types_ptr' for NULL after already dereferencing it [-Wanalyzer-deref-before-check] 35 | if (odr_types_ptr) | ^ 'get_odr_type': events 1-3 | | 27 | if (cond) | | ^ | | | | | (1) following 'false' branch... |...... | 31 | else if (other_cond) | | ~~~~~~~~~~~ | | || | | |(2) ...to here | | (3) following 'true' branch... | 'get_odr_type': event 4 | | 11 | #define odr_types (*odr_types_ptr) | | ~^~~~~~~~~~~~~~~ | | | | | (4) ...to here deref-before-check-pr77425.c:33:7: note: in expansion of macro 'odr_types' | 33 | odr_types[val->id] = 0; | | ^~~~~~~~~ | 'get_odr_type': event 5 | | 11 | #define odr_types (*odr_types_ptr) | | ~^~~~~~~~~~~~~~~ | | | | | (5) pointer 'odr_types_ptr' is dereferenced here deref-before-check-pr77425.c:33:7: note: in expansion of macro 'odr_types' | 33 | odr_types[val->id] = 0; | | ^~~~~~~~~ | 'get_odr_type': event 6 | | 35 | if (odr_types_ptr) | | ^ | | | | | (6) pointer 'odr_types_ptr' is checked for NULL here but it was already dereferenced at (5) | gcc/analyzer/ChangeLog: PR analyzer/99671 * analyzer.opt (Wanalyzer-deref-before-check): New warning. * diagnostic-manager.cc (null_assignment_sm_context::set_next_state): Only add state change events for transition to "null" state. (null_assignment_sm_context::is_transition_to_null): New. * engine.cc (impl_region_model_context::on_pop_frame): New. * exploded-graph.h (impl_region_model_context::on_pop_frame): New decl. * program-state.cc (sm_state_map::clear_any_state): New. (sm_state_map::can_merge_with_p): New. (program_state::can_merge_with_p): Replace requirement that sm-states be equal in favor of an attempt to merge them. * program-state.h (sm_state_map::clear_any_state): New decl. (sm_state_map::can_merge_with_p): New decl. * region-model.cc (region_model::eval_condition): Make const. (region_model::pop_frame): Call ctxt->on_pop_frame. * region-model.h (region_model::eval_condition): Make const. (region_model_context::on_pop_frame): New vfunc. (noop_region_model_context::on_pop_frame): New. (region_model_context_decorator::on_pop_frame): New. * sm-malloc.cc (enum resource_state): Add RS_ASSUMED_NON_NULL. (allocation_state::dump_to_pp): Drop "final". (struct assumed_non_null_state): New subclass. (malloc_state_machine::m_assumed_non_null): New. (assumed_non_null_p): New. (class deref_before_check): New. (assumed_non_null_state::dump_to_pp): New. (malloc_state_machine::get_or_create_assumed_non_null_state_for_frame): New. (malloc_state_machine::maybe_assume_non_null): New. (malloc_state_machine::on_stmt): Transition from start state to "assumed-non-null" state for pointers passed to __attribute__((nonnull)) arguments, and for pointers explicitly dereferenced. Call maybe_complain_about_deref_before_check for pointers explicitly compared against NULL. (malloc_state_machine::maybe_complain_about_deref_before_check): New. (malloc_state_machine::on_deallocator_call): Also transition "assumed-non-null" states to "freed". (malloc_state_machine::on_pop_frame): New. (malloc_state_machine::maybe_get_merged_states_nonequal): New. * sm-malloc.dot: Update for changes to sm-malloc.cc. * sm.h (state_machine::on_pop_frame): New. (state_machine::maybe_get_merged_state): New. (state_machine::maybe_get_merged_states_nonequal): New. gcc/ChangeLog: * doc/gcc/gcc-command-options/options-that-control-static-analysis.rst: Add -Wanalyzer-deref-before-check. gcc/testsuite/ChangeLog: * gcc.dg/analyzer/deref-before-check-1.c: New test. * gcc.dg/analyzer/deref-before-check-2.c: New test. * gcc.dg/analyzer/deref-before-check-pr77425.c: New test. * gcc.dg/analyzer/malloc-1.c (test_51): New test. gcc/ChangeLog: PR analyzer/99671 * tristate.h (tristate::is_unknown): New. Signed-off-by: David Malcolm <dmalcolm@redhat.com>
Diffstat (limited to 'gcc/analyzer/sm-malloc.cc')
-rw-r--r--gcc/analyzer/sm-malloc.cc314
1 files changed, 310 insertions, 4 deletions
diff --git a/gcc/analyzer/sm-malloc.cc b/gcc/analyzer/sm-malloc.cc
index d050ef8..dd10356 100644
--- a/gcc/analyzer/sm-malloc.cc
+++ b/gcc/analyzer/sm-malloc.cc
@@ -87,6 +87,9 @@ enum resource_state
/* The start state. */
RS_START,
+ /* State for a pointer that's been unconditionally dereferenced. */
+ RS_ASSUMED_NON_NULL,
+
/* State for a pointer that's known to be NULL. */
RS_NULL,
@@ -126,7 +129,7 @@ struct allocation_state : public state_machine::state
m_deallocator (deallocator)
{}
- void dump_to_pp (pretty_printer *pp) const final override;
+ void dump_to_pp (pretty_printer *pp) const override;
const allocation_state *get_nonnull () const;
@@ -135,6 +138,25 @@ struct allocation_state : public state_machine::state
const deallocator *m_deallocator;
};
+/* Custom state subclass, for the "assumed-non-null" state
+ where the assumption happens in a particular frame. */
+
+struct assumed_non_null_state : public allocation_state
+{
+ assumed_non_null_state (const char *name, unsigned id,
+ const frame_region *frame)
+ : allocation_state (name, id, RS_ASSUMED_NON_NULL,
+ NULL, NULL),
+ m_frame (frame)
+ {
+ gcc_assert (m_frame);
+ }
+
+ void dump_to_pp (pretty_printer *pp) const final override;
+
+ const frame_region *m_frame;
+};
+
/* An enum for choosing which wording to use in various diagnostics
when describing deallocations. */
@@ -384,14 +406,25 @@ public:
enum tree_code op,
const svalue *rhs) const final override;
+ void on_pop_frame (sm_state_map *smap,
+ const frame_region *) const final override;
+
bool can_purge_p (state_t s) const final override;
std::unique_ptr<pending_diagnostic> on_leak (tree var) const final override;
bool reset_when_passed_to_unknown_fn_p (state_t s,
bool is_mutable) const final override;
+ state_t
+ maybe_get_merged_states_nonequal (state_t state_a,
+ state_t state_b) const final override;
+
static bool unaffected_by_call_p (tree fndecl);
+ void maybe_assume_non_null (sm_context *sm_ctxt,
+ tree ptr,
+ const gimple *stmt) const;
+
void on_realloc_with_move (region_model *model,
sm_state_map *smap,
const svalue *old_ptr_sval,
@@ -406,6 +439,10 @@ public:
/* States that are independent of api. */
+ /* States for a pointer that's been unconditionally dereferenced
+ in a particular stack frame. */
+ hash_map<const frame_region *, state_t> m_assumed_non_null;
+
/* State for a pointer that's known to be NULL. */
state_t m_null;
@@ -425,6 +462,16 @@ private:
const deallocator *
get_or_create_deallocator (tree deallocator_fndecl);
+ state_t
+ get_or_create_assumed_non_null_state_for_frame (const frame_region *frame);
+
+ void
+ maybe_complain_about_deref_before_check (sm_context *sm_ctxt,
+ const supernode *node,
+ const gimple *stmt,
+ const assumed_non_null_state *,
+ tree ptr) const;
+
void on_allocator_call (sm_context *sm_ctxt,
const gcall *call,
const deallocator_set *deallocators,
@@ -678,6 +725,14 @@ freed_p (state_machine::state_t state)
return get_rs (state) == RS_FREED;
}
+/* Return true if STATE is a value that has been assumed to be non-NULL. */
+
+static bool
+assumed_non_null_p (state_machine::state_t state)
+{
+ return get_rs (state) == RS_ASSUMED_NON_NULL;
+}
+
/* Class for diagnostics relating to malloc_state_machine. */
class malloc_diagnostic : public pending_diagnostic
@@ -1428,6 +1483,80 @@ private:
const char *m_funcname;
};
+/* Concrete pending_diagnostic subclass for -Wanalyzer-deref-before-check. */
+
+class deref_before_check : public malloc_diagnostic
+{
+public:
+ deref_before_check (const malloc_state_machine &sm, tree arg)
+ : malloc_diagnostic (sm, arg)
+ {}
+
+ const char *get_kind () const final override { return "deref_before_check"; }
+
+ int get_controlling_option () const final override
+ {
+ return OPT_Wanalyzer_deref_before_check;
+ }
+
+ bool emit (rich_location *rich_loc) final override
+ {
+ if (m_arg)
+ return warning_at (rich_loc, get_controlling_option (),
+ "check of %qE for NULL after already"
+ " dereferencing it",
+ m_arg);
+ else
+ return warning_at (rich_loc, get_controlling_option (),
+ "check of pointer for NULL after already"
+ " dereferencing it");
+ }
+
+ label_text describe_state_change (const evdesc::state_change &change)
+ final override
+ {
+ if (change.m_old_state == m_sm.get_start_state ()
+ && assumed_non_null_p (change.m_new_state))
+ {
+ m_first_deref_event = change.m_event_id;
+ if (m_arg)
+ return change.formatted_print ("pointer %qE is dereferenced here",
+ m_arg);
+ else
+ return label_text::borrow ("pointer is dereferenced here");
+ }
+ return malloc_diagnostic::describe_state_change (change);
+ }
+
+ label_text describe_final_event (const evdesc::final_event &ev) final override
+ {
+ if (m_first_deref_event.known_p ())
+ {
+ if (m_arg)
+ return ev.formatted_print ("pointer %qE is checked for NULL here but"
+ " it was already dereferenced at %@",
+ m_arg, &m_first_deref_event);
+ else
+ return ev.formatted_print ("pointer is checked for NULL here but"
+ " it was already dereferenced at %@",
+ &m_first_deref_event);
+ }
+ else
+ {
+ if (m_arg)
+ return ev.formatted_print ("pointer %qE is checked for NULL here but"
+ " it was already dereferenced",
+ m_arg);
+ else
+ return ev.formatted_print ("pointer is checked for NULL here but"
+ " it was already dereferenced");
+ }
+ }
+
+private:
+ diagnostic_event_id_t m_first_deref_event;
+};
+
/* struct allocation_state : public state_machine::state. */
/* Implementation of state_machine::state::dump_to_pp vfunc
@@ -1456,6 +1585,17 @@ allocation_state::get_nonnull () const
return as_a_allocation_state (m_deallocators->m_nonnull);
}
+/* struct assumed_non_null_state : public allocation_state. */
+
+void
+assumed_non_null_state::dump_to_pp (pretty_printer *pp) const
+{
+ allocation_state::dump_to_pp (pp);
+ pp_string (pp, " (in ");
+ m_frame->dump_to_pp (pp, true);
+ pp_character (pp, ')');
+}
+
/* malloc_state_machine's ctor. */
malloc_state_machine::malloc_state_machine (logger *logger)
@@ -1597,6 +1737,22 @@ malloc_state_machine::get_or_create_deallocator (tree deallocator_fndecl)
return d;
}
+/* Get the "assumed-non-null" state for assumptions made within FRAME,
+ creating it if necessary. */
+
+state_machine::state_t
+malloc_state_machine::
+get_or_create_assumed_non_null_state_for_frame (const frame_region *frame)
+{
+ if (state_t *slot = m_assumed_non_null.get (frame))
+ return *slot;
+ state_machine::state *new_state
+ = new assumed_non_null_state ("assumed-non-null", alloc_state_id (), frame);
+ add_custom_state (new_state);
+ m_assumed_non_null.put (frame, new_state);
+ return new_state;
+}
+
/* Try to identify the function declaration either by name or as a known malloc
builtin. */
@@ -1629,6 +1785,33 @@ known_allocator_p (const_tree fndecl, const gcall *call)
return false;
}
+/* If PTR's nullness is not known, transition it to the "assumed-non-null"
+ state for the current frame. */
+
+void
+malloc_state_machine::maybe_assume_non_null (sm_context *sm_ctxt,
+ tree ptr,
+ const gimple *stmt) const
+{
+ const region_model *old_model = sm_ctxt->get_old_region_model ();
+ if (!old_model)
+ return;
+
+ tree null_ptr_cst = build_int_cst (TREE_TYPE (ptr), 0);
+ tristate known_non_null
+ = old_model->eval_condition (ptr, NE_EXPR, null_ptr_cst, NULL);
+ if (known_non_null.is_unknown ())
+ {
+ /* Cast away const-ness for cache-like operations. */
+ malloc_state_machine *mut_this
+ = const_cast <malloc_state_machine *> (this);
+ state_t next_state
+ = mut_this->get_or_create_assumed_non_null_state_for_frame
+ (old_model->get_current_frame ());
+ sm_ctxt->set_next_state (stmt, ptr, next_state);
+ }
+}
+
/* Implementation of state_machine::on_stmt vfunc for malloc_state_machine. */
bool
@@ -1743,6 +1926,8 @@ malloc_state_machine::on_stmt (sm_context *sm_ctxt,
(*this, diag_arg, callee_fndecl, i));
sm_ctxt->set_next_state (stmt, arg, m_stop);
}
+ else if (state == m_start)
+ maybe_assume_non_null (sm_ctxt, arg, stmt);
}
}
BITMAP_FREE (nonnull_args);
@@ -1760,6 +1945,36 @@ malloc_state_machine::on_stmt (sm_context *sm_ctxt,
}
}
+ /* Look for pointers explicitly being compared against zero
+ that are in state assumed_non_null i.e. we already defererenced
+ them.
+ We have to do this check here, rather than in on_condition
+ because we add a constraint that the pointer is non-null when
+ dereferencing it, and this makes the apply_constraints_for_gcond
+ find known-true and known-false conditions; on_condition is only
+ called when adding new constraints. */
+ if (const gcond *cond_stmt = dyn_cast <const gcond *> (stmt))
+ {
+ enum tree_code op = gimple_cond_code (cond_stmt);
+ if (op == EQ_EXPR || op == NE_EXPR)
+ {
+ tree lhs = gimple_cond_lhs (cond_stmt);
+ tree rhs = gimple_cond_rhs (cond_stmt);
+ if (any_pointer_p (lhs)
+ && any_pointer_p (rhs)
+ && zerop (rhs))
+ {
+ state_t state = sm_ctxt->get_state (stmt, lhs);
+ if (assumed_non_null_p (state))
+ maybe_complain_about_deref_before_check
+ (sm_ctxt, node,
+ stmt,
+ (const assumed_non_null_state *)state,
+ lhs);
+ }
+ }
+ }
+
if (tree lhs = sm_ctxt->is_zero_assignment (stmt))
if (any_pointer_p (lhs))
on_zero_assignment (sm_ctxt, stmt,lhs);
@@ -1778,7 +1993,9 @@ malloc_state_machine::on_stmt (sm_context *sm_ctxt,
tree arg = TREE_OPERAND (op, 0);
state_t state = sm_ctxt->get_state (stmt, arg);
- if (unchecked_p (state))
+ if (state == m_start)
+ maybe_assume_non_null (sm_ctxt, arg, stmt);
+ else if (unchecked_p (state))
{
tree diag_arg = sm_ctxt->get_diagnostic_tree (arg);
sm_ctxt->warn (node, stmt, arg,
@@ -1808,6 +2025,53 @@ malloc_state_machine::on_stmt (sm_context *sm_ctxt,
return false;
}
+/* Given a check against null of PTR in assumed-non-null state STATE,
+ potentially add a deref_before_check warning to SM_CTXT. */
+
+void
+malloc_state_machine::
+maybe_complain_about_deref_before_check (sm_context *sm_ctxt,
+ const supernode *node,
+ const gimple *stmt,
+ const assumed_non_null_state *state,
+ tree ptr) const
+{
+ const region_model *model = sm_ctxt->get_old_region_model ();
+ if (!model)
+ return;
+
+ /* Don't complain if the current frame (where the check is occurring) is
+ deeper than the frame in which the "not null" assumption was made.
+ This suppress false positives for cases like:
+
+ void foo (struct s *p)
+ {
+ int val = s->some_field; // deref here
+ shared_helper (p);
+ }
+
+ where "shared_helper" has:
+
+ void shared_helper (struct s *p)
+ {
+ if (!p) // check here
+ return;
+ // etc
+ }
+
+ since the check in "shared_helper" is OK. */
+ const frame_region *checked_in_frame = model->get_current_frame ();
+ const frame_region *assumed_nonnull_in_frame = state->m_frame;
+ if (checked_in_frame->get_index () > assumed_nonnull_in_frame->get_index ())
+ return;
+
+ tree diag_ptr = sm_ctxt->get_diagnostic_tree (ptr);
+ sm_ctxt->warn
+ (node, stmt, ptr,
+ make_unique<deref_before_check> (*this, diag_ptr));
+ sm_ctxt->set_next_state (stmt, ptr, m_stop);
+}
+
/* Handle a call to an allocator.
RETURNS_NONNULL is true if CALL is to a fndecl known to have
__attribute__((returns_nonnull)). */
@@ -1870,8 +2134,8 @@ malloc_state_machine::on_deallocator_call (sm_context *sm_ctxt,
state_t state = sm_ctxt->get_state (call, arg);
- /* start/unchecked/nonnull -> freed. */
- if (state == m_start)
+ /* start/assumed_non_null/unchecked/nonnull -> freed. */
+ if (state == m_start || assumed_non_null_p (state))
sm_ctxt->set_next_state (call, arg, d->m_freed);
else if (unchecked_p (state) || nonnull_p (state))
{
@@ -2016,6 +2280,31 @@ malloc_state_machine::on_condition (sm_context *sm_ctxt,
}
}
+/* Implementation of state_machine::on_pop_frame vfunc for malloc_state_machine.
+ Clear any "assumed-non-null" state where the assumption happened in
+ FRAME_REG. */
+
+void
+malloc_state_machine::on_pop_frame (sm_state_map *smap,
+ const frame_region *frame_reg) const
+{
+ hash_set<const svalue *> svals_to_clear;
+ for (auto kv : *smap)
+ {
+ const svalue *sval = kv.first;
+ state_t state = kv.second.m_state;
+ if (assumed_non_null_p (state))
+ {
+ const assumed_non_null_state *assumed_state
+ = (const assumed_non_null_state *)state;
+ if (frame_reg == assumed_state->m_frame)
+ svals_to_clear.add (sval);
+ }
+ }
+ for (auto sval : svals_to_clear)
+ smap->clear_any_state (sval);
+}
+
/* Implementation of state_machine::can_purge_p vfunc for malloc_state_machine.
Don't allow purging of pointers in state 'unchecked' or 'nonnull'
(to avoid false leak reports). */
@@ -2053,6 +2342,23 @@ malloc_state_machine::reset_when_passed_to_unknown_fn_p (state_t s,
return is_mutable;
}
+/* Implementation of state_machine::maybe_get_merged_states_nonequal vfunc
+ for malloc_state_machine.
+
+ Support discarding "assumed-non-null" states when merging with
+ start state. */
+
+state_machine::state_t
+malloc_state_machine::maybe_get_merged_states_nonequal (state_t state_a,
+ state_t state_b) const
+{
+ if (assumed_non_null_p (state_a) && state_b == m_start)
+ return m_start;
+ if (state_a == m_start && assumed_non_null_p (state_b))
+ return m_start;
+ return NULL;
+}
+
/* Return true if calls to FNDECL are known to not affect this sm-state. */
bool