From c7e276b869bdeb4a95735c1f037ee1a5f629de3d Mon Sep 17 00:00:00 2001 From: David Malcolm Date: Mon, 18 Jan 2021 09:24:46 -0500 Subject: [PATCH] analyzer: use "malloc" attribute In dce6c58db87ebf7f4477bd3126228e73e4eeee97 msebor extended the "malloc" attribute to support user-defined allocator/deallocator pairs. This patch extends the "malloc" checker within -fanalyzer to use these attributes. It is based on an earlier patch: 'RFC: add "deallocated_by" attribute for use by analyzer' https://gcc.gnu.org/pipermail/gcc-patches/2020-October/555544.html which added a different attribute. The patch needed a lot of reworking to support multiple deallocators per allocator. My hope was that this would provide a minimal level of markup that would support library-checking without requiring lots of further markup. I attempted to use this to detect a memory leak within a Linux driver (CVE-2019-19078), by adding the attribute to mark these fns: extern struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags); extern void usb_free_urb(struct urb *urb); where there is a leak of a "urb" on an error-handling path. Unfortunately I ran into the problem that there are various other fns that take "struct urb *" and the analyzer conservatively assumes that a urb passed to them might or might not be freed and thus stops tracking state for them. Hence this will only detect issues for the simplest cases (without adding another attribute). gcc/analyzer/ChangeLog: * analyzer.h (is_std_named_call_p): New decl. * diagnostic-manager.cc (path_builder::get_sm): New. (state_change_event_creator::state_change_event_creator): Add "pb" param. (state_change_event_creator::on_global_state_change): Don't consider state changes affecting other state_machines. (state_change_event_creator::on_state_change): Likewise. (state_change_event_creator::m_pb): New field. (diagnostic_manager::add_events_for_eedge): Pass pb to visitor ctor. * region-model-impl-calls.cc (region_model::impl_deallocation_call): New. * region-model.cc: Include "attribs.h". (region_model::on_call_post): Handle fndecls referenced by __attribute__((deallocated_by(FOO))). * region-model.h (region_model::impl_deallocation_call): New decl. * sm-malloc.cc: Include "stringpool.h" and "attribs.h". Add leading comment. (class api): Delete. (enum resource_state): Update comment for change from api to deallocator and deallocator_set. (allocation_state::allocation_state): Drop api param. Add "deallocators" and "deallocator". (allocation_state::m_api): Drop field in favor of... (allocation_state::m_deallocators): New field. (allocation_state::m_deallocator): New field. (enum wording): Add WORDING_DEALLOCATED. (struct deallocator): New. (struct standard_deallocator): New. (struct custom_deallocator): New. (struct deallocator_set): New. (struct custom_deallocator_set): New. (struct standard_deallocator_set): New. (struct deallocator_set_map_traits): New. (malloc_state_machine::m_malloc): Drop field (malloc_state_machine::m_scalar_new): Likewise. (malloc_state_machine::m_vector_new): Likewise. (malloc_state_machine::m_free): New field (malloc_state_machine::m_scalar_delete): Likewise. (malloc_state_machine::m_vector_delete): Likewise. (malloc_state_machine::deallocator_map_t): New typedef. (malloc_state_machine::m_deallocator_map): New field. (malloc_state_machine::deallocator_set_cache_t): New typedef. (malloc_state_machine::m_custom_deallocator_set_cache): New field. (malloc_state_machine::custom_deallocator_set_map_t): New typedef. (malloc_state_machine::m_custom_deallocator_set_map): New field. (malloc_state_machine::m_dynamic_sets): New field. (malloc_state_machine::m_dynamic_deallocators): New field. (api::api): Delete. (deallocator::deallocator): New ctor. (deallocator::hash): New. (deallocator::dump_to_pp): New. (deallocator::cmp): New. (deallocator::cmp_ptr_ptr): New. (standard_deallocator::standard_deallocator): New ctor. (deallocator_set::deallocator_set): New ctor. (deallocator_set::dump): New. (custom_deallocator_set::custom_deallocator_set): New ctor. (custom_deallocator_set::contains_p): New. (custom_deallocator_set::maybe_get_single): New. (custom_deallocator_set::dump_to_pp): New. (standard_deallocator_set::standard_deallocator_set): New ctor. (standard_deallocator_set::contains_p): New. (standard_deallocator_set::maybe_get_single): New. (standard_deallocator_set::dump_to_pp): New. (start_p): New. (class mismatching_deallocation): Update for conversion from api to deallocator_set and deallocator. (double_free::emit): Use %qs. (class use_after_free): Update for conversion from api to deallocator_set and deallocator. (malloc_leak::describe_state_change): Only emit "allocated here" on a start->nonnull transition, rather than on other transitions to nonnull. (allocation_state::dump_to_pp): Update for conversion from api to deallocator_set. (allocation_state::get_nonnull): Likewise. (malloc_state_machine::malloc_state_machine): Likewise. (malloc_state_machine::~malloc_state_machine): New. (malloc_state_machine::add_state): Update for conversion from api to deallocator_set. (malloc_state_machine::get_or_create_custom_deallocator_set): New. (malloc_state_machine::maybe_create_custom_deallocator_set): New. (malloc_state_machine::get_or_create_deallocator): New. (malloc_state_machine::on_stmt): Update for conversion from api to deallocator_set. Handle "__attribute__((malloc(FOO)))", and the special attribute set on FOO. (malloc_state_machine::on_allocator_call): Update for conversion from api to deallocator_set. Add "returns_nonnull" param and use it to affect which state to transition to. (malloc_state_machine::on_deallocator_call): Update for conversion from api to deallocator_set. gcc/ChangeLog: * attribs.h (fndecl_dealloc_argno): New decl. * builtins.c (call_dealloc_argno): Split out second half of function into... (fndecl_dealloc_argno): New. * doc/extend.texi (Common Function Attributes): Document the interaction between the analyzer and the malloc attribute. * doc/invoke.texi (Static Analyzer Options): Likewise. gcc/testsuite/ChangeLog: * gcc.dg/analyzer/attr-malloc-1.c: New test. * gcc.dg/analyzer/attr-malloc-2.c: New test. * gcc.dg/analyzer/attr-malloc-4.c: New test. * gcc.dg/analyzer/attr-malloc-5.c: New test. * gcc.dg/analyzer/attr-malloc-6.c: New test. * gcc.dg/analyzer/attr-malloc-CVE-2019-19078-usb-leak.c: New test. * gcc.dg/analyzer/attr-malloc-misuses.c: New test. --- gcc/analyzer/analyzer.h | 1 + gcc/analyzer/diagnostic-manager.cc | 15 +- gcc/analyzer/region-model-impl-calls.cc | 9 + gcc/analyzer/region-model.cc | 9 + gcc/analyzer/region-model.h | 1 + gcc/analyzer/sm-malloc.cc | 767 +++++++++++++++--- gcc/attribs.h | 2 + gcc/builtins.c | 10 + gcc/doc/extend.texi | 52 ++ gcc/doc/invoke.texi | 14 +- gcc/testsuite/gcc.dg/analyzer/attr-malloc-1.c | 75 ++ gcc/testsuite/gcc.dg/analyzer/attr-malloc-2.c | 24 + gcc/testsuite/gcc.dg/analyzer/attr-malloc-4.c | 21 + gcc/testsuite/gcc.dg/analyzer/attr-malloc-5.c | 12 + gcc/testsuite/gcc.dg/analyzer/attr-malloc-6.c | 228 ++++++ .../attr-malloc-CVE-2019-19078-usb-leak.c | 224 +++++ .../gcc.dg/analyzer/attr-malloc-misuses.c | 18 + 17 files changed, 1354 insertions(+), 128 deletions(-) create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-malloc-1.c create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-malloc-2.c create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-malloc-4.c create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-malloc-5.c create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-malloc-6.c create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-malloc-CVE-2019-19078-usb-leak.c create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-malloc-misuses.c diff --git a/gcc/analyzer/analyzer.h b/gcc/analyzer/analyzer.h index 6996092717c..f50ac662516 100644 --- a/gcc/analyzer/analyzer.h +++ b/gcc/analyzer/analyzer.h @@ -205,6 +205,7 @@ extern bool is_special_named_call_p (const gcall *call, const char *funcname, extern bool is_named_call_p (tree fndecl, const char *funcname); extern bool is_named_call_p (tree fndecl, const char *funcname, const gcall *call, unsigned int num_args); +extern bool is_std_named_call_p (tree fndecl, const char *funcname); extern bool is_std_named_call_p (tree fndecl, const char *funcname, const gcall *call, unsigned int num_args); extern bool is_setjmp_call_p (const gcall *call); diff --git a/gcc/analyzer/diagnostic-manager.cc b/gcc/analyzer/diagnostic-manager.cc index 3625e3fe746..22ca024f28e 100644 --- a/gcc/analyzer/diagnostic-manager.cc +++ b/gcc/analyzer/diagnostic-manager.cc @@ -189,6 +189,8 @@ public: return m_feasibility_problem; } + const state_machine *get_sm () const { return m_sd.m_sm; } + private: typedef reachability enode_reachability; @@ -709,9 +711,11 @@ diagnostic_manager::build_emission_path (const path_builder &pb, class state_change_event_creator : public state_change_visitor { public: - state_change_event_creator (const exploded_edge &eedge, + state_change_event_creator (const path_builder &pb, + const exploded_edge &eedge, checker_path *emission_path) - : m_eedge (eedge), + : m_pb (pb), + m_eedge (eedge), m_emission_path (emission_path) {} @@ -720,6 +724,8 @@ public: state_machine::state_t dst_sm_val) FINAL OVERRIDE { + if (&sm != m_pb.get_sm ()) + return false; const exploded_node *src_node = m_eedge.m_src; const program_point &src_point = src_node->get_point (); const int src_stack_depth = src_point.get_stack_depth (); @@ -748,6 +754,8 @@ public: const svalue *sval, const svalue *dst_origin_sval) FINAL OVERRIDE { + if (&sm != m_pb.get_sm ()) + return false; const exploded_node *src_node = m_eedge.m_src; const program_point &src_point = src_node->get_point (); const int src_stack_depth = src_point.get_stack_depth (); @@ -783,6 +791,7 @@ public: return false; } + const path_builder &m_pb; const exploded_edge &m_eedge; checker_path *m_emission_path; }; @@ -1002,7 +1011,7 @@ diagnostic_manager::add_events_for_eedge (const path_builder &pb, | ~~~~~~~~~~~~~^~~~~ | | | (3) ...to here (end_cfg_edge_event). */ - state_change_event_creator visitor (eedge, emission_path); + state_change_event_creator visitor (pb, eedge, emission_path); for_each_state_change (src_state, dst_state, pb.get_ext_state (), &visitor); diff --git a/gcc/analyzer/region-model-impl-calls.cc b/gcc/analyzer/region-model-impl-calls.cc index 1b81c742c72..d3b66ba7375 100644 --- a/gcc/analyzer/region-model-impl-calls.cc +++ b/gcc/analyzer/region-model-impl-calls.cc @@ -436,6 +436,15 @@ region_model::impl_call_strlen (const call_details &cd) return true; } +/* Handle calls to functions referenced by + __attribute__((malloc(FOO))). */ + +void +region_model::impl_deallocation_call (const call_details &cd) +{ + impl_call_free (cd); +} + } // namespace ana #endif /* #if ENABLE_ANALYZER */ diff --git a/gcc/analyzer/region-model.cc b/gcc/analyzer/region-model.cc index 81e1a481b51..cfe8a391dd9 100644 --- a/gcc/analyzer/region-model.cc +++ b/gcc/analyzer/region-model.cc @@ -65,6 +65,7 @@ along with GCC; see the file COPYING3. If not see #include "analyzer/region-model-reachability.h" #include "analyzer/analyzer-selftests.h" #include "stor-layout.h" +#include "attribs.h" #if ENABLE_ANALYZER @@ -917,6 +918,14 @@ region_model::on_call_post (const gcall *call, impl_call_operator_delete (cd); return; } + /* Was this fndecl referenced by + __attribute__((malloc(FOO)))? */ + if (lookup_attribute ("*dealloc", DECL_ATTRIBUTES (callee_fndecl))) + { + call_details cd (call, this, ctxt); + impl_deallocation_call (cd); + return; + } } if (unknown_side_effects) diff --git a/gcc/analyzer/region-model.h b/gcc/analyzer/region-model.h index 0e303d0398a..efd1a09e362 100644 --- a/gcc/analyzer/region-model.h +++ b/gcc/analyzer/region-model.h @@ -463,6 +463,7 @@ class region_model bool impl_call_strlen (const call_details &cd); bool impl_call_operator_new (const call_details &cd); bool impl_call_operator_delete (const call_details &cd); + void impl_deallocation_call (const call_details &cd); void handle_unrecognized_call (const gcall *call, region_model_context *ctxt); diff --git a/gcc/analyzer/sm-malloc.cc b/gcc/analyzer/sm-malloc.cc index e1fea02486f..d7c76e343ff 100644 --- a/gcc/analyzer/sm-malloc.cc +++ b/gcc/analyzer/sm-malloc.cc @@ -42,6 +42,8 @@ along with GCC; see the file COPYING3. If not see #include "analyzer/program-point.h" #include "analyzer/store.h" #include "analyzer/region-model.h" +#include "stringpool.h" +#include "attribs.h" #if ENABLE_ANALYZER @@ -49,14 +51,38 @@ namespace ana { namespace { -class api; +/* This state machine and its various support classes track allocations + and deallocations. + + It has a few standard allocation/deallocation pairs (e.g. new/delete), + and also supports user-defined ones via + __attribute__ ((malloc(DEALLOCATOR))). + + There can be more than one valid deallocator for a given allocator, + for example: + __attribute__ ((malloc (fclose))) + __attribute__ ((malloc (freopen, 3))) + FILE* fopen (const char*, const char*); + A deallocator_set represents a particular set of valid deallocators. + + We track the expected deallocator_set for a value, but not the allocation + function - there could be more than one allocator per deallocator_set. + For example, there could be dozens of allocators for "free" beyond just + malloc e.g. calloc, xstrdup, etc. We don't want to explode the number + of states by tracking individual allocators in the exploded graph; + we merely want to track "this value expects to have 'free' called on it". + Perhaps we can reconstruct which allocator was used later, when emitting + the path, if it's necessary for precision of wording of diagnostics. */ + +class deallocator; +class deallocator_set; class malloc_state_machine; /* An enum for discriminating between different kinds of allocation_state. */ enum resource_state { - /* States that are independent of api. */ + /* States that are independent of allocator/deallocator. */ /* The start state. */ RS_START, @@ -71,28 +97,33 @@ enum resource_state /* Stop state, for pointers we don't want to track any more. */ RS_STOP, - /* States that relate to a specific api. */ + /* States that relate to a specific deallocator_set. */ - /* State for a pointer returned from the api's allocator that hasn't + /* State for a pointer returned from an allocator that hasn't been checked for NULL. It could be a pointer to heap-allocated memory, or could be NULL. */ RS_UNCHECKED, - /* State for a pointer returned from the api's allocator, + /* State for a pointer returned from an allocator, known to be non-NULL. */ RS_NONNULL, - /* State for a pointer passed to the api's deallocator. */ + /* State for a pointer passed to a deallocator. */ RS_FREED }; -/* Custom state subclass, which can optionally refer to an an api. */ +/* Custom state subclass, which can optionally refer to an a + deallocator_set. */ struct allocation_state : public state_machine::state { allocation_state (const char *name, unsigned id, - enum resource_state rs, const api *a) - : state (name, id), m_rs (rs), m_api (a) + enum resource_state rs, + const deallocator_set *deallocators, + const deallocator *deallocator) + : state (name, id), m_rs (rs), + m_deallocators (deallocators), + m_deallocator (deallocator) {} void dump_to_pp (pretty_printer *pp) const FINAL OVERRIDE; @@ -100,7 +131,8 @@ struct allocation_state : public state_machine::state const allocation_state *get_nonnull () const; enum resource_state m_rs; - const api *m_api; + const deallocator_set *m_deallocators; + const deallocator *m_deallocator; }; /* An enum for choosing which wording to use in various diagnostics @@ -109,52 +141,186 @@ struct allocation_state : public state_machine::state enum wording { WORDING_FREED, - WORDING_DELETED + WORDING_DELETED, + WORDING_DEALLOCATED }; -/* Represents a particular family of API calls for allocating/deallocating - heap memory that should be matched e.g. - - malloc/free - - scalar new/delete - - vector new[]/delete[] - etc. +/* Base class representing a deallocation function, + either a built-in one we know about, or one exposed via + __attribute__((malloc(DEALLOCATOR))). */ - We track the expected deallocation function, but not the allocation - function - there could be more than one allocator per deallocator. For - example, there could be dozens of allocators for "free" beyond just - malloc e.g. calloc, xstrdup, etc. We don't want to explode the number - of states by tracking individual allocators in the exploded graph; - we merely want to track "this value expects to have 'free' called on it". - Perhaps we can reconstruct which allocator was used later, when emitting - the path, if it's necessary for precision of wording of diagnostics. */ - -struct api +struct deallocator { - api (malloc_state_machine *sm, - const char *name, - const char *dealloc_funcname, - enum wording wording); + hashval_t hash () const; + void dump_to_pp (pretty_printer *pp) const; + static int cmp (const deallocator *a, const deallocator *b); + static int cmp_ptr_ptr (const void *, const void *); - /* An internal name for identifying this API in dumps. */ + /* Name to use in diagnostics. */ const char *m_name; - /* The name of the deallocation function, for use in diagnostics. */ - const char *m_dealloc_funcname; + /* Which wording to use in diagnostics. */ + enum wording m_wording; + + /* State for a value passed to one of the deallocators. */ + state_machine::state_t m_freed; + +protected: + deallocator (malloc_state_machine *sm, + const char *name, + enum wording wording); +}; + +/* Subclass representing a predefined deallocator. + e.g. "delete []", without needing a specific FUNCTION_DECL + ahead of time. */ + +struct standard_deallocator : public deallocator +{ + standard_deallocator (malloc_state_machine *sm, + const char *name, + enum wording wording); +}; + +/* Subclass representing a user-defined deallocator + via __attribute__((malloc(DEALLOCATOR))) given + a specific FUNCTION_DECL. */ + +struct custom_deallocator : public deallocator +{ + custom_deallocator (malloc_state_machine *sm, + tree deallocator_fndecl, + enum wording wording) + : deallocator (sm, IDENTIFIER_POINTER (DECL_NAME (deallocator_fndecl)), + wording) + { + } +}; + +/* Base class representing a set of possible deallocators. + Often this will be just a single deallocator, but some + allocators have multiple valid deallocators (e.g. the result of + "fopen" can be closed by either "fclose" or "freopen"). */ + +struct deallocator_set +{ + deallocator_set (malloc_state_machine *sm, + enum wording wording); + virtual ~deallocator_set () {} + + virtual bool contains_p (const deallocator *d) const = 0; + virtual const deallocator *maybe_get_single () const = 0; + virtual void dump_to_pp (pretty_printer *pp) const = 0; + void dump () const; /* Which wording to use in diagnostics. */ enum wording m_wording; - /* Pointers to api-specific states. + /* Pointers to states. These states are owned by the state_machine base class. */ - /* State for an unchecked result from this api's allocator. */ + /* State for an unchecked result from an allocator using this set. */ state_machine::state_t m_unchecked; - /* State for a known non-NULL result from this apis's allocator. */ + /* State for a known non-NULL result from such an allocator. */ state_machine::state_t m_nonnull; +}; - /* State for a value passed to this api's deallocator. */ - state_machine::state_t m_freed; +/* Subclass of deallocator_set representing a set of deallocators + defined by one or more __attribute__((malloc(DEALLOCATOR))). */ + +struct custom_deallocator_set : public deallocator_set +{ + typedef const auto_vec *key_t; + + custom_deallocator_set (malloc_state_machine *sm, + const auto_vec *vec, + //const char *name, + //const char *dealloc_funcname, + //unsigned arg_idx, + enum wording wording); + + bool contains_p (const deallocator *d) const FINAL OVERRIDE; + const deallocator *maybe_get_single () const FINAL OVERRIDE; + void dump_to_pp (pretty_printer *pp) const FINAL OVERRIDE; + + auto_vec m_deallocator_vec; +}; + +/* Subclass of deallocator_set representing a set of deallocators + with a single standard_deallocator, e.g. "delete []". */ + +struct standard_deallocator_set : public deallocator_set +{ + standard_deallocator_set (malloc_state_machine *sm, + const char *name, + enum wording wording); + + bool contains_p (const deallocator *d) const FINAL OVERRIDE; + const deallocator *maybe_get_single () const FINAL OVERRIDE; + void dump_to_pp (pretty_printer *pp) const FINAL OVERRIDE; + + standard_deallocator m_deallocator; +}; + +/* Traits class for ensuring uniqueness of deallocator_sets within + malloc_state_machine. */ + +struct deallocator_set_map_traits +{ + typedef custom_deallocator_set::key_t key_type; + typedef custom_deallocator_set *value_type; + typedef custom_deallocator_set *compare_type; + + static inline hashval_t hash (const key_type &k) + { + gcc_assert (k != NULL); + gcc_assert (k != reinterpret_cast (1)); + + hashval_t result = 0; + unsigned i; + const deallocator *d; + FOR_EACH_VEC_ELT (*k, i, d) + result ^= d->hash (); + return result; + } + static inline bool equal_keys (const key_type &k1, const key_type &k2) + { + if (k1->length () != k2->length ()) + return false; + + for (unsigned i = 0; i < k1->length (); i++) + if ((*k1)[i] != (*k2)[i]) + return false; + + return true; + } + template + static inline void remove (T &) + { + /* empty; the nodes are handled elsewhere. */ + } + template + static inline void mark_deleted (T &entry) + { + entry.m_key = reinterpret_cast (1); + } + template + static inline void mark_empty (T &entry) + { + entry.m_key = NULL; + } + template + static inline bool is_deleted (const T &entry) + { + return entry.m_key == reinterpret_cast (1); + } + template + static inline bool is_empty (const T &entry) + { + return entry.m_key == NULL; + } + static const bool empty_zero_p = false; }; /* A state machine for detecting misuses of the malloc/free API. @@ -167,9 +333,12 @@ public: typedef allocation_state custom_data_t; malloc_state_machine (logger *logger); + ~malloc_state_machine (); state_t - add_state (const char *name, enum resource_state rs, const api *a); + add_state (const char *name, enum resource_state rs, + const deallocator_set *deallocators, + const deallocator *deallocator); bool inherited_state_p () const FINAL OVERRIDE { return false; } @@ -214,9 +383,9 @@ public: bool reset_when_passed_to_unknown_fn_p (state_t s, bool is_mutable) const FINAL OVERRIDE; - api m_malloc; - api m_scalar_new; - api m_vector_new; + standard_deallocator_set m_free; + standard_deallocator_set m_scalar_delete; + standard_deallocator_set m_vector_delete; /* States that are independent of api. */ @@ -232,33 +401,194 @@ public: state_t m_stop; private: + const custom_deallocator_set * + get_or_create_custom_deallocator_set (tree allocator_fndecl); + custom_deallocator_set * + maybe_create_custom_deallocator_set (tree allocator_fndecl); + const deallocator * + get_or_create_deallocator (tree deallocator_fndecl); + void on_allocator_call (sm_context *sm_ctxt, const gcall *call, - const api &ap) const; + const deallocator_set *deallocators, + bool returns_nonnull = false) const; void on_deallocator_call (sm_context *sm_ctxt, const supernode *node, const gcall *call, - const api &ap) const; + const deallocator *d, + unsigned argno) const; void on_zero_assignment (sm_context *sm_ctxt, const gimple *stmt, tree lhs) const; + + /* A map for consolidating deallocators so that they are + unique per deallocator FUNCTION_DECL. */ + typedef hash_map deallocator_map_t; + deallocator_map_t m_deallocator_map; + + /* Memoized lookups from FUNCTION_DECL to custom_deallocator_set *. */ + typedef hash_map deallocator_set_cache_t; + deallocator_set_cache_t m_custom_deallocator_set_cache; + + /* A map for consolidating custom_deallocator_set instances. */ + typedef hash_map custom_deallocator_set_map_t; + custom_deallocator_set_map_t m_custom_deallocator_set_map; + + /* Record of dynamically-allocated objects, for cleanup. */ + auto_vec m_dynamic_sets; + auto_vec m_dynamic_deallocators; }; -/* struct api. */ +/* struct deallocator. */ -api::api (malloc_state_machine *sm, - const char *name, - const char *dealloc_funcname, - enum wording wording) +deallocator::deallocator (malloc_state_machine *sm, + const char *name, + enum wording wording) : m_name (name), - m_dealloc_funcname (dealloc_funcname), m_wording (wording), - m_unchecked (sm->add_state ("unchecked", RS_UNCHECKED, this)), - m_nonnull (sm->add_state ("nonnull", RS_NONNULL, this)), - m_freed (sm->add_state ("freed", RS_FREED, this)) + m_freed (sm->add_state ("freed", RS_FREED, NULL, this)) +{ +} + +hashval_t +deallocator::hash () const +{ + return (hashval_t)m_freed->get_id (); +} + +void +deallocator::dump_to_pp (pretty_printer *pp) const +{ + pp_printf (pp, "%qs", m_name); +} + +int +deallocator::cmp (const deallocator *a, const deallocator *b) +{ + return (int)a->m_freed->get_id () - (int)b->m_freed->get_id (); +} + +int +deallocator::cmp_ptr_ptr (const void *a, const void *b) +{ + return cmp (*(const deallocator * const *)a, + *(const deallocator * const *)b); +} + + +/* struct standard_deallocator : public deallocator. */ + +standard_deallocator::standard_deallocator (malloc_state_machine *sm, + const char *name, + enum wording wording) +: deallocator (sm, name, wording) +{ +} + +/* struct deallocator_set. */ + +deallocator_set::deallocator_set (malloc_state_machine *sm, + enum wording wording) +: m_wording (wording), + m_unchecked (sm->add_state ("unchecked", RS_UNCHECKED, this, NULL)), + m_nonnull (sm->add_state ("nonnull", RS_NONNULL, this, NULL)) +{ +} + +/* Dump a description of this deallocator_set to stderr. */ + +DEBUG_FUNCTION void +deallocator_set::dump () const +{ + pretty_printer pp; + pp_show_color (&pp) = pp_show_color (global_dc->printer); + pp.buffer->stream = stderr; + dump_to_pp (&pp); + pp_newline (&pp); + pp_flush (&pp); +} + +/* struct custom_deallocator_set : public deallocator_set. */ + +custom_deallocator_set:: +custom_deallocator_set (malloc_state_machine *sm, + const auto_vec *vec, + enum wording wording) +: deallocator_set (sm, wording), + m_deallocator_vec (vec->length ()) +{ + unsigned i; + const deallocator *d; + FOR_EACH_VEC_ELT (*vec, i, d) + m_deallocator_vec.safe_push (d); +} + +bool +custom_deallocator_set::contains_p (const deallocator *d) const +{ + unsigned i; + const deallocator *cd; + FOR_EACH_VEC_ELT (m_deallocator_vec, i, cd) + if (cd == d) + return true; + return false; +} + +const deallocator * +custom_deallocator_set::maybe_get_single () const +{ + if (m_deallocator_vec.length () == 1) + return m_deallocator_vec[0]; + return NULL; +} + +void +custom_deallocator_set::dump_to_pp (pretty_printer *pp) const +{ + pp_character (pp, '{'); + unsigned i; + const deallocator *d; + FOR_EACH_VEC_ELT (m_deallocator_vec, i, d) + { + if (i > 0) + pp_string (pp, ", "); + d->dump_to_pp (pp); + } + pp_character (pp, '}'); +} + +/* struct standard_deallocator_set : public deallocator_set. */ + +standard_deallocator_set::standard_deallocator_set (malloc_state_machine *sm, + const char *name, + enum wording wording) +: deallocator_set (sm, wording), + m_deallocator (sm, name, wording) { } +bool +standard_deallocator_set::contains_p (const deallocator *d) const +{ + return d == &m_deallocator; +} + +const deallocator * +standard_deallocator_set::maybe_get_single () const +{ + return &m_deallocator; +} + +void +standard_deallocator_set::dump_to_pp (pretty_printer *pp) const +{ + pp_character (pp, '{'); + pp_string (pp, m_deallocator.m_name); + pp_character (pp, '}'); +} + /* Return STATE cast to the custom state subclass, or NULL for the start state. Everything should be an allocation_state apart from the start state. */ @@ -291,6 +621,14 @@ get_rs (state_machine::state_t state) return RS_START; } +/* Return true if STATE is the start state. */ + +static bool +start_p (state_machine::state_t state) +{ + return get_rs (state) == RS_START; +} + /* Return true if STATE is an unchecked result from an allocator. */ static bool @@ -383,10 +721,10 @@ class mismatching_deallocation : public malloc_diagnostic { public: mismatching_deallocation (const malloc_state_machine &sm, tree arg, - const api *expected_dealloc, - const api *actual_dealloc) + const deallocator_set *expected_deallocators, + const deallocator *actual_dealloc) : malloc_diagnostic (sm, arg), - m_expected_dealloc (expected_dealloc), + m_expected_deallocators (expected_deallocators), m_actual_dealloc (actual_dealloc) {} @@ -400,11 +738,18 @@ public: auto_diagnostic_group d; diagnostic_metadata m; m.add_cwe (762); /* CWE-762: Mismatched Memory Management Routines. */ - return warning_meta (rich_loc, m, OPT_Wanalyzer_mismatching_deallocation, - "%qE should have been deallocated with %qs" - " but was deallocated with %qs", - m_arg, m_expected_dealloc->m_dealloc_funcname, - m_actual_dealloc->m_dealloc_funcname); + if (const deallocator *expected_dealloc + = m_expected_deallocators->maybe_get_single ()) + return warning_meta (rich_loc, m, OPT_Wanalyzer_mismatching_deallocation, + "%qE should have been deallocated with %qs" + " but was deallocated with %qs", + m_arg, expected_dealloc->m_name, + m_actual_dealloc->m_name); + else + return warning_meta (rich_loc, m, OPT_Wanalyzer_mismatching_deallocation, + "%qs called on %qE returned from a mismatched" + " allocation function", + m_actual_dealloc->m_name, m_arg); } label_text describe_state_change (const evdesc::state_change &change) @@ -413,9 +758,13 @@ public: if (unchecked_p (change.m_new_state)) { m_alloc_event = change.m_event_id; - return change.formatted_print ("allocated here" - " (expects deallocation with %qs)", - m_expected_dealloc->m_dealloc_funcname); + if (const deallocator *expected_dealloc + = m_expected_deallocators->maybe_get_single ()) + return change.formatted_print ("allocated here" + " (expects deallocation with %qs)", + expected_dealloc->m_name); + else + return change.formatted_print ("allocated here"); } return malloc_diagnostic::describe_state_change (change); } @@ -423,19 +772,28 @@ public: label_text describe_final_event (const evdesc::final_event &ev) FINAL OVERRIDE { if (m_alloc_event.known_p ()) - return ev.formatted_print - ("deallocated with %qs here;" - " allocation at %@ expects deallocation with %qs", - m_actual_dealloc->m_dealloc_funcname, &m_alloc_event, - m_expected_dealloc->m_dealloc_funcname); + { + if (const deallocator *expected_dealloc + = m_expected_deallocators->maybe_get_single ()) + return ev.formatted_print + ("deallocated with %qs here;" + " allocation at %@ expects deallocation with %qs", + m_actual_dealloc->m_name, &m_alloc_event, + expected_dealloc->m_name); + else + return ev.formatted_print + ("deallocated with %qs here;" + " allocated at %@", + m_actual_dealloc->m_name, &m_alloc_event); + } return ev.formatted_print ("deallocated with %qs here", - m_actual_dealloc->m_dealloc_funcname); + m_actual_dealloc->m_name); } private: diagnostic_event_id_t m_alloc_event; - const api *m_expected_dealloc; - const api *m_actual_dealloc; + const deallocator_set *m_expected_deallocators; + const deallocator *m_actual_dealloc; }; /* Concrete subclass for reporting double-free diagnostics. */ @@ -455,7 +813,7 @@ public: diagnostic_metadata m; m.add_cwe (415); /* CWE-415: Double Free. */ return warning_meta (rich_loc, m, OPT_Wanalyzer_double_free, - "double-%<%s%> of %qE", m_funcname, m_arg); + "double-%qs of %qE", m_funcname, m_arg); } label_text describe_state_change (const evdesc::state_change &change) @@ -765,9 +1123,12 @@ class use_after_free : public malloc_diagnostic { public: use_after_free (const malloc_state_machine &sm, tree arg, - const api *a) - : malloc_diagnostic (sm, arg), m_api (a) - {} + const deallocator *deallocator) + : malloc_diagnostic (sm, arg), + m_deallocator (deallocator) + { + gcc_assert (deallocator); + } const char *get_kind () const FINAL OVERRIDE { return "use_after_free"; } @@ -778,7 +1139,7 @@ public: m.add_cwe (416); return warning_meta (rich_loc, m, OPT_Wanalyzer_use_after_free, "use after %<%s%> of %qE", - m_api->m_dealloc_funcname, m_arg); + m_deallocator->m_name, m_arg); } label_text describe_state_change (const evdesc::state_change &change) @@ -787,7 +1148,7 @@ public: if (freed_p (change.m_new_state)) { m_free_event = change.m_event_id; - switch (m_api->m_wording) + switch (m_deallocator->m_wording) { default: gcc_unreachable (); @@ -795,6 +1156,8 @@ public: return label_text::borrow ("freed here"); case WORDING_DELETED: return label_text::borrow ("deleted here"); + case WORDING_DEALLOCATED: + return label_text::borrow ("deallocated here"); } } return malloc_diagnostic::describe_state_change (change); @@ -802,9 +1165,9 @@ public: label_text describe_final_event (const evdesc::final_event &ev) FINAL OVERRIDE { - const char *funcname = m_api->m_dealloc_funcname; + const char *funcname = m_deallocator->m_name; if (m_free_event.known_p ()) - switch (m_api->m_wording) + switch (m_deallocator->m_wording) { default: gcc_unreachable (); @@ -814,6 +1177,10 @@ public: case WORDING_DELETED: return ev.formatted_print ("use after %<%s%> of %qE; deleted at %@", funcname, ev.m_expr, &m_free_event); + case WORDING_DEALLOCATED: + return ev.formatted_print ("use after %<%s%> of %qE;" + " deallocated at %@", + funcname, ev.m_expr, &m_free_event); } else return ev.formatted_print ("use after %<%s%> of %qE", @@ -822,7 +1189,7 @@ public: private: diagnostic_event_id_t m_free_event; - const api *m_api; + const deallocator *m_deallocator; }; class malloc_leak : public malloc_diagnostic @@ -848,7 +1215,8 @@ public: label_text describe_state_change (const evdesc::state_change &change) FINAL OVERRIDE { - if (unchecked_p (change.m_new_state)) + if (unchecked_p (change.m_new_state) + || (start_p (change.m_old_state) && nonnull_p (change.m_new_state))) { m_alloc_event = change.m_event_id; return label_text::borrow ("allocated here"); @@ -969,40 +1337,161 @@ void allocation_state::dump_to_pp (pretty_printer *pp) const { state_machine::state::dump_to_pp (pp); - if (m_api) - pp_printf (pp, " (%s)", m_api->m_name); + if (m_deallocators) + { + pp_string (pp, " ("); + m_deallocators->dump_to_pp (pp); + pp_character (pp, ')'); + } } -/* Given a allocation_state for an api, get the "nonnull" state - for the corresponding allocator. */ +/* Given a allocation_state for a deallocator_set, get the "nonnull" state + for the corresponding allocator(s). */ const allocation_state * allocation_state::get_nonnull () const { - gcc_assert (m_api); - return as_a_allocation_state (m_api->m_nonnull); + gcc_assert (m_deallocators); + return as_a_allocation_state (m_deallocators->m_nonnull); } /* malloc_state_machine's ctor. */ malloc_state_machine::malloc_state_machine (logger *logger) : state_machine ("malloc", logger), - m_malloc (this, "malloc", "free", WORDING_FREED), - m_scalar_new (this, "new", "delete", WORDING_DELETED), - m_vector_new (this, "new[]", "delete[]", WORDING_DELETED) + m_free (this, "free", WORDING_FREED), + m_scalar_delete (this, "delete", WORDING_DELETED), + m_vector_delete (this, "delete[]", WORDING_DELETED) { gcc_assert (m_start->get_id () == 0); - m_null = add_state ("null", RS_FREED, NULL); - m_non_heap = add_state ("non-heap", RS_NON_HEAP, NULL); - m_stop = add_state ("stop", RS_STOP, NULL); + m_null = add_state ("null", RS_FREED, NULL, NULL); + m_non_heap = add_state ("non-heap", RS_NON_HEAP, NULL, NULL); + m_stop = add_state ("stop", RS_STOP, NULL, NULL); +} + +malloc_state_machine::~malloc_state_machine () +{ + unsigned i; + custom_deallocator_set *set; + FOR_EACH_VEC_ELT (m_dynamic_sets, i, set) + delete set; + custom_deallocator *d; + FOR_EACH_VEC_ELT (m_dynamic_deallocators, i, d) + delete d; } state_machine::state_t malloc_state_machine::add_state (const char *name, enum resource_state rs, - const api *a) + const deallocator_set *deallocators, + const deallocator *deallocator) { return add_custom_state (new allocation_state (name, alloc_state_id (), - rs, a)); + rs, deallocators, + deallocator)); +} + +/* If ALLOCATOR_FNDECL has any "__attribute__((malloc(FOO)))", + return a custom_deallocator_set for them, consolidating them + to ensure uniqueness of the sets. + + Return NULL if it has no such attributes. */ + +const custom_deallocator_set * +malloc_state_machine:: +get_or_create_custom_deallocator_set (tree allocator_fndecl) +{ + /* Early rejection of decls without attributes. */ + tree attrs = DECL_ATTRIBUTES (allocator_fndecl); + if (!attrs) + return NULL; + + /* Otherwise, call maybe_create_custom_deallocator_set, + memoizing the result. */ + if (custom_deallocator_set **slot + = m_custom_deallocator_set_cache.get (allocator_fndecl)) + return *slot; + custom_deallocator_set *set + = maybe_create_custom_deallocator_set (allocator_fndecl); + m_custom_deallocator_set_cache.put (allocator_fndecl, set); + return set; +} + +/* Given ALLOCATOR_FNDECL, a FUNCTION_DECL with attributes, + look for any "__attribute__((malloc(FOO)))" and return a + custom_deallocator_set for them, consolidating them + to ensure uniqueness of the sets. + + Return NULL if it has no such attributes. + + Subroutine of get_or_create_custom_deallocator_set which + memoizes the result. */ + +custom_deallocator_set * +malloc_state_machine:: +maybe_create_custom_deallocator_set (tree allocator_fndecl) +{ + tree attrs = DECL_ATTRIBUTES (allocator_fndecl); + gcc_assert (attrs); + + /* Look for instances of __attribute__((malloc(FOO))). */ + auto_vec deallocator_vec; + for (tree allocs = attrs; + (allocs = lookup_attribute ("malloc", allocs)); + allocs = TREE_CHAIN (allocs)) + { + tree args = TREE_VALUE (allocs); + if (!args) + continue; + if (TREE_VALUE (args)) + { + const deallocator *d + = get_or_create_deallocator (TREE_VALUE (args)); + deallocator_vec.safe_push (d); + } + } + + /* If there weren't any deallocators, bail. */ + if (deallocator_vec.length () == 0) + return NULL; + + /* Consolidate, so that we reuse existing deallocator_set + instances. */ + deallocator_vec.qsort (deallocator::cmp_ptr_ptr); + custom_deallocator_set **slot + = m_custom_deallocator_set_map.get (&deallocator_vec); + if (slot) + return *slot; + custom_deallocator_set *set + = new custom_deallocator_set (this, &deallocator_vec, WORDING_DEALLOCATED); + m_custom_deallocator_set_map.put (&set->m_deallocator_vec, set); + m_dynamic_sets.safe_push (set); + return set; +} + +/* Get the deallocator for DEALLOCATOR_FNDECL, creating it if necessary. */ + +const deallocator * +malloc_state_machine::get_or_create_deallocator (tree deallocator_fndecl) +{ + deallocator **slot = m_deallocator_map.get (deallocator_fndecl); + if (slot) + return *slot; + + /* Reuse "free". */ + deallocator *d; + if (is_named_call_p (deallocator_fndecl, "free") + || is_std_named_call_p (deallocator_fndecl, "free")) + d = &m_free.m_deallocator; + else + { + custom_deallocator *cd + = new custom_deallocator (this, deallocator_fndecl, + WORDING_DEALLOCATED); + m_dynamic_deallocators.safe_push (cd); + d = cd; + } + m_deallocator_map.put (deallocator_fndecl, d); + return d; } /* Implementation of state_machine::on_stmt vfunc for malloc_state_machine. */ @@ -1024,23 +1513,25 @@ malloc_state_machine::on_stmt (sm_context *sm_ctxt, || is_named_call_p (callee_fndecl, "strdup", call, 1) || is_named_call_p (callee_fndecl, "strndup", call, 2)) { - on_allocator_call (sm_ctxt, call, m_malloc); + on_allocator_call (sm_ctxt, call, &m_free); return true; } if (is_named_call_p (callee_fndecl, "operator new", call, 1)) - on_allocator_call (sm_ctxt, call, m_scalar_new); + on_allocator_call (sm_ctxt, call, &m_scalar_delete); else if (is_named_call_p (callee_fndecl, "operator new []", call, 1)) - on_allocator_call (sm_ctxt, call, m_vector_new); + on_allocator_call (sm_ctxt, call, &m_vector_delete); else if (is_named_call_p (callee_fndecl, "operator delete", call, 1) || is_named_call_p (callee_fndecl, "operator delete", call, 2)) { - on_deallocator_call (sm_ctxt, node, call, m_scalar_new); + on_deallocator_call (sm_ctxt, node, call, + &m_scalar_delete.m_deallocator, 0); return true; } else if (is_named_call_p (callee_fndecl, "operator delete []", call, 1)) { - on_deallocator_call (sm_ctxt, node, call, m_vector_new); + on_deallocator_call (sm_ctxt, node, call, + &m_vector_delete.m_deallocator, 0); return true; } @@ -1057,10 +1548,26 @@ malloc_state_machine::on_stmt (sm_context *sm_ctxt, || is_std_named_call_p (callee_fndecl, "free", call, 1) || is_named_call_p (callee_fndecl, "__builtin_free", call, 1)) { - on_deallocator_call (sm_ctxt, node, call, m_malloc); + on_deallocator_call (sm_ctxt, node, call, + &m_free.m_deallocator, 0); return true; } + /* Cast away const-ness for cache-like operations. */ + malloc_state_machine *mutable_this + = const_cast (this); + + /* Handle "__attribute__((malloc(FOO)))". */ + if (const deallocator_set *deallocators + = mutable_this->get_or_create_custom_deallocator_set + (callee_fndecl)) + { + tree attrs = TYPE_ATTRIBUTES (TREE_TYPE (callee_fndecl)); + bool returns_nonnull + = lookup_attribute ("returns_nonnull", attrs); + on_allocator_call (sm_ctxt, call, deallocators, returns_nonnull); + } + /* Handle "__attribute__((nonnull))". */ { tree fntype = TREE_TYPE (callee_fndecl); @@ -1103,6 +1610,16 @@ malloc_state_machine::on_stmt (sm_context *sm_ctxt, BITMAP_FREE (nonnull_args); } } + + /* Check for this after nonnull, so that if we have both + then we transition to "freed", rather than "checked". */ + unsigned dealloc_argno = fndecl_dealloc_argno (callee_fndecl); + if (dealloc_argno != UINT_MAX) + { + const deallocator *d + = mutable_this->get_or_create_deallocator (callee_fndecl); + on_deallocator_call (sm_ctxt, node, call, d, dealloc_argno); + } } if (tree lhs = sm_ctxt->is_zero_assignment (stmt)) @@ -1162,7 +1679,7 @@ malloc_state_machine::on_stmt (sm_context *sm_ctxt, const allocation_state *astate = as_a_allocation_state (state); sm_ctxt->warn (node, stmt, arg, new use_after_free (*this, diag_arg, - astate->m_api)); + astate->m_deallocator)); sm_ctxt->set_next_state (stmt, arg, m_stop); } } @@ -1170,18 +1687,24 @@ malloc_state_machine::on_stmt (sm_context *sm_ctxt, return false; } -/* Handle a call to an allocator. */ +/* Handle a call to an allocator. + RETURNS_NONNULL is true if CALL is to a fndecl known to have + __attribute__((returns_nonnull)). */ void malloc_state_machine::on_allocator_call (sm_context *sm_ctxt, const gcall *call, - const api &ap) const + const deallocator_set *deallocators, + bool returns_nonnull) const { tree lhs = gimple_call_lhs (call); if (lhs) { if (sm_ctxt->get_state (call, lhs) == m_start) - sm_ctxt->set_next_state (call, lhs, ap.m_unchecked); + sm_ctxt->set_next_state (call, lhs, + (returns_nonnull + ? deallocators->m_nonnull + : deallocators->m_unchecked)); } else { @@ -1193,40 +1716,42 @@ void malloc_state_machine::on_deallocator_call (sm_context *sm_ctxt, const supernode *node, const gcall *call, - const api &ap) const + const deallocator *d, + unsigned argno) const { - tree arg = gimple_call_arg (call, 0); + if (argno >= gimple_call_num_args (call)) + return; + tree arg = gimple_call_arg (call, argno); tree diag_arg = sm_ctxt->get_diagnostic_tree (arg); state_t state = sm_ctxt->get_state (call, arg); /* start/unchecked/nonnull -> freed. */ if (state == m_start) - sm_ctxt->set_next_state (call, arg, ap.m_freed); + sm_ctxt->set_next_state (call, arg, d->m_freed); else if (unchecked_p (state) || nonnull_p (state)) { const allocation_state *astate = as_a_allocation_state (state); - - if (astate->m_api != &ap) + gcc_assert (astate->m_deallocators); + if (!astate->m_deallocators->contains_p (d)) { /* Wrong allocator. */ - pending_diagnostic *d + pending_diagnostic *pd = new mismatching_deallocation (*this, diag_arg, - astate->m_api, &ap); - sm_ctxt->warn (node, call, arg, d); + astate->m_deallocators, + d); + sm_ctxt->warn (node, call, arg, pd); } - sm_ctxt->set_next_state (call, arg, ap.m_freed); + sm_ctxt->set_next_state (call, arg, d->m_freed); } /* Keep state "null" as-is, rather than transitioning to "freed"; we don't want to complain about double-free of NULL. */ - - else if (state == ap.m_freed) + else if (state == d->m_freed) { /* freed -> stop, with warning. */ sm_ctxt->warn (node, call, arg, - new double_free (*this, diag_arg, - ap.m_dealloc_funcname)); + new double_free (*this, diag_arg, d->m_name)); sm_ctxt->set_next_state (call, arg, m_stop); } else if (state == m_non_heap) @@ -1234,7 +1759,7 @@ malloc_state_machine::on_deallocator_call (sm_context *sm_ctxt, /* non-heap -> stop, with warning. */ sm_ctxt->warn (node, call, arg, new free_of_non_heap (*this, diag_arg, - ap.m_dealloc_funcname)); + d->m_name)); sm_ctxt->set_next_state (call, arg, m_stop); } } diff --git a/gcc/attribs.h b/gcc/attribs.h index 9e3e56a73fb..21d28a47f39 100644 --- a/gcc/attribs.h +++ b/gcc/attribs.h @@ -310,4 +310,6 @@ extern void init_attr_rdwr_indices (rdwr_map *, tree); extern attr_access *get_parm_access (rdwr_map &, tree, tree = current_function_decl); +extern unsigned fndecl_dealloc_argno (tree fndecl); + #endif // GCC_ATTRIBS_H diff --git a/gcc/builtins.c b/gcc/builtins.c index 02e7815aee5..c1115a32d91 100644 --- a/gcc/builtins.c +++ b/gcc/builtins.c @@ -13014,6 +13014,16 @@ call_dealloc_argno (tree exp) if (!fndecl) return UINT_MAX; + return fndecl_dealloc_argno (fndecl); +} + +/* Return the zero-based number corresponding to the argument being + deallocated if FNDECL is a deallocation function or UINT_MAX + if it isn't. */ + +unsigned +fndecl_dealloc_argno (tree fndecl) +{ /* A call to operator delete isn't recognized as one to a built-in. */ if (DECL_IS_OPERATOR_DELETE_P (fndecl)) return 0; diff --git a/gcc/doc/extend.texi b/gcc/doc/extend.texi index c5b1faff60b..8daa1c67974 100644 --- a/gcc/doc/extend.texi +++ b/gcc/doc/extend.texi @@ -3291,6 +3291,58 @@ __attribute__ ((malloc, malloc (fclose (1)))) FILE* tmpfile (void); @end smallexample +The warnings guarded by @option{-fanalyzer} respect allocation and +deallocation pairs marked with the @code{malloc}. In particular: + +@itemize @bullet + +@item +The analyzer will emit a @option{-Wanalyzer-mismatching-deallocation} +diagnostic if there is an execution path in which the result of an +allocation call is passed to a different deallocator. + +@item +The analyzer will emit a @option{-Wanalyzer-double-free} +diagnostic if there is an execution path in which a value is passed +more than once to a deallocation call. + +@item +The analyzer will consider the possibility that an allocation function +could fail and return NULL. It will emit +@option{-Wanalyzer-possible-null-dereference} and +@option{-Wanalyzer-possible-null-argument} diagnostics if there are +execution paths in which an unchecked result of an allocation call is +dereferenced or passed to a function requiring a non-null argument. +If the allocator always returns non-null, use +@code{__attribute__ ((returns_nonnull))} to suppress these warnings. +For example: +@smallexample +char *xstrdup (const char *) + __attribute__((malloc (free), returns_nonnull)); +@end smallexample + +@item +The analyzer will emit a @option{-Wanalyzer-use-after-free} +diagnostic if there is an execution path in which the memory passed +by pointer to a deallocation call is used after the deallocation. + +@item +The analyzer will emit a @option{-Wanalyzer-malloc-leak} diagnostic if +there is an execution path in which the result of an allocation call +is leaked (without being passed to the deallocation function). + +@item +The analyzer will emit a @option{-Wanalyzer-free-of-non-heap} diagnostic +if a deallocation function is used on a global or on-stack variable. + +@end itemize + +The analyzer assumes that deallocators can gracefully handle the @code{NULL} +pointer. If this is not the case, the deallocator can be marked with +@code{__attribute__((nonnull))} so that @option{-fanalyzer} can emit +a @option{-Wanalyzer-possible-null-argument} diagnostic for code paths +in which the deallocator is called with NULL. + @item no_icf @cindex @code{no_icf} function attribute This function attribute prevents a functions from being merged with another diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi index c290b6f4938..5077ea7df13 100644 --- a/gcc/doc/invoke.texi +++ b/gcc/doc/invoke.texi @@ -9155,7 +9155,8 @@ This warning requires @option{-fanalyzer}, which enables it; use @option{-Wno-analyzer-double-free} to disable it. This diagnostic warns for paths through the code in which a pointer -can have @code{free} called on it more than once. +can have a deallocator called on it more than once, either @code{free}, +or a deallocator referenced by attribute @code{malloc}. @item -Wno-analyzer-exposure-through-output-file @opindex Wanalyzer-exposure-through-output-file @@ -9196,7 +9197,8 @@ This warning requires @option{-fanalyzer}, which enables it; use to disable it. This diagnostic warns for paths through the code in which a -pointer allocated via @code{malloc} is leaked. +pointer allocated via an allocator is leaked: either @code{malloc}, +or a function marked with attribute @code{malloc}. @item -Wno-analyzer-mismatching-deallocation @opindex Wanalyzer-mismatching-deallocation @@ -9207,7 +9209,10 @@ to disable it. This diagnostic warns for paths through the code in which the wrong deallocation function is called on a pointer value, based on -which function was used to allocate the pointer value. +which function was used to allocate the pointer value. The diagnostic +will warn about mismatches between @code{free}, scalar @code{delete} +and vector @code{delete[]}, and those marked as allocator/deallocator +pairs using attribute @code{malloc}. @item -Wno-analyzer-possible-null-argument @opindex Wanalyzer-possible-null-argument @@ -9322,7 +9327,8 @@ This warning requires @option{-fanalyzer}, which enables it; use @option{-Wno-analyzer-use-after-free} to disable it. This diagnostic warns for paths through the code in which a -pointer is used after @code{free} is called on it. +pointer is used after a deallocator is called on it: either @code{free}, +or a deallocator referenced by attribute @code{malloc}. @item -Wno-analyzer-use-of-pointer-in-stale-stack-frame @opindex Wanalyzer-use-of-pointer-in-stale-stack-frame diff --git a/gcc/testsuite/gcc.dg/analyzer/attr-malloc-1.c b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-1.c new file mode 100644 index 00000000000..3de32b1b14b --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-1.c @@ -0,0 +1,75 @@ +extern void free (void *); + +struct foo +{ + int m_int; +}; + +extern void foo_release (struct foo *); +extern struct foo *foo_acquire (void) + __attribute__ ((malloc (foo_release))); +extern void use_foo (const struct foo *) + __attribute__((nonnull)); + +void test_1 (void) +{ + struct foo *p = foo_acquire (); + foo_release (p); +} + +void test_2 (void) +{ + struct foo *p = foo_acquire (); /* { dg-message "this call could return NULL" } */ + p->m_int = 42; /* { dg-warning "dereference of possibly-NULL 'p'" } */ + foo_release (p); +} + +void test_2a (void) +{ + struct foo *p = foo_acquire (); /* { dg-message "this call could return NULL" } */ + use_foo (p); /* { dg-warning "use of possibly-NULL 'p' where non-null expected" } */ + foo_release (p); +} + +void test_3 (void) +{ + struct foo *p = foo_acquire (); /* { dg-message "allocated here" } */ +} /* { dg-warning "leak of 'p'" } */ + +void test_4 (struct foo *p) +{ + foo_release (p); + foo_release (p); /* { dg-warning "double-'foo_release' of 'p'" } */ +} + +void test_4a (void) +{ + struct foo *p = foo_acquire (); + foo_release (p); + foo_release (p); /* { dg-warning "double-'foo_release' of 'p'" } */ +} + +void test_5 (void) +{ + struct foo *p = foo_acquire (); /* { dg-message "allocated here \\(expects deallocation with 'foo_release'\\)" } */ + free (p); /* { dg-warning "'p' should have been deallocated with 'foo_release' but was deallocated with 'free'" } */ +} + +void test_6 (struct foo *p) +{ + foo_release (p); + free (p); // TODO: double-release warning! +} + +void test_7 () +{ + struct foo f; + foo_release (&f); /* { dg-warning "not on the heap" "analyzer" } */ + /* { dg-warning "'foo_release' called on unallocated object 'f'" "non-analyzer" { target *-*-* } .-1 } */ +} + +int test_8 (struct foo *p) +{ + foo_release (p); + return p->m_int; /* { dg-warning "use after 'foo_release' of 'p'" } */ +} diff --git a/gcc/testsuite/gcc.dg/analyzer/attr-malloc-2.c b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-2.c new file mode 100644 index 00000000000..09d941fe082 --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-2.c @@ -0,0 +1,24 @@ +extern void free (void *); +char *xstrdup (const char *) + __attribute__((malloc (free), returns_nonnull)); + +void test_1 (const char *s) +{ + char *p = xstrdup (s); + free (p); +} + +/* Verify that we don't issue -Wanalyzer-possible-null-dereference + when the allocator has __attribute__((returns_nonnull)). */ + +char *test_2 (const char *s) +{ + char *p = xstrdup (s); + p[0] = 'a'; /* { dg-bogus "possibly-NULL" } */ + return p; +} + +void test_3 (const char *s) +{ + char *p = xstrdup (s); /* { dg-message "allocated here" } */ +} /* { dg-warning "leak of 'p'" } */ diff --git a/gcc/testsuite/gcc.dg/analyzer/attr-malloc-4.c b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-4.c new file mode 100644 index 00000000000..1517667dfb2 --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-4.c @@ -0,0 +1,21 @@ +/* An example where the deallocator requires non-NULL. */ + +struct foo; +extern void foo_release (struct foo *) + __attribute__((nonnull)); +extern struct foo *foo_acquire (void) + __attribute__ ((malloc (foo_release))); + +void test_1 (void) +{ + struct foo *p = foo_acquire (); /* { dg-message "this call could return NULL" } */ + foo_release (p); /* { dg-warning "use of possibly-NULL 'p' where non-null" } */ +} + +void test_2 (void) +{ + struct foo *p = foo_acquire (); + if (!p) + return; + foo_release (p); +} diff --git a/gcc/testsuite/gcc.dg/analyzer/attr-malloc-5.c b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-5.c new file mode 100644 index 00000000000..7ff4e57fcfb --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-5.c @@ -0,0 +1,12 @@ +/* Example of extra argument to "malloc" attribute. */ + +struct foo; +extern void foo_release (int, struct foo *); +extern struct foo *foo_acquire (void) + __attribute__ ((malloc (foo_release, 2))); + +void test_1 (void) +{ + struct foo *p = foo_acquire (); + foo_release (0, p); +} diff --git a/gcc/testsuite/gcc.dg/analyzer/attr-malloc-6.c b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-6.c new file mode 100644 index 00000000000..bd28107d0d7 --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-6.c @@ -0,0 +1,228 @@ +/* Adapted from gcc.dg/Wmismatched-dealloc.c. */ + +#define A(...) __attribute__ ((malloc (__VA_ARGS__))) + +typedef struct FILE FILE; +typedef __SIZE_TYPE__ size_t; + +void free (void*); +void* malloc (size_t); +void* realloc (void*, size_t); + +int fclose (FILE*); +FILE* freopen (const char*, const char*, FILE*); +int pclose (FILE*); + +A (fclose) A (freopen, 3) + FILE* fdopen (int); +A (fclose) A (freopen, 3) + FILE* fopen (const char*, const char*); +A (fclose) A (freopen, 3) + FILE* fmemopen(void *, size_t, const char *); +A (fclose) A (freopen, 3) + FILE* freopen (const char*, const char*, FILE*); +A (pclose) A (freopen, 3) + FILE* popen (const char*, const char*); +A (fclose) A (freopen, 3) + FILE* tmpfile (void); + +void sink (FILE*); + + + void release (void*); +A (release) FILE* acquire (void); + +void nowarn_fdopen (void) +{ + { + FILE *q = fdopen (0); + if (!q) + return; + + fclose (q); + } + + { + FILE *q = fdopen (0); + if (!q) + return; + + q = freopen ("1", "r", q); + fclose (q); + } + + { + FILE *q = fdopen (0); + if (!q) + return; + + sink (q); + } +} + + +void warn_fdopen (void) +{ + { + FILE *q = fdopen (0); // { dg-message "allocated here" } + release (q); // { dg-warning "'release' called on 'q' returned from a mismatched allocation function" } + } + { + FILE *q = fdopen (0); // { dg-message "allocated here" } + free (q); // { dg-warning "'free' called on 'q' returned from a mismatched allocation function" } + } + + { + FILE *q = fdopen (0); // { dg-message "allocated here" } + q = realloc (q, 7); // { dg-warning "'realloc' called on 'q' returned from a mismatched allocation function" } + sink (q); + } +} + + +void nowarn_fopen (void) +{ + { + FILE *q = fopen ("1", "r"); + sink (q); + fclose (q); + } + + { + FILE *q = fopen ("2", "r"); + sink (q); + q = freopen ("3", "r", q); + sink (q); + fclose (q); + } + + { + FILE *q = fopen ("4", "r"); + sink (q); + } +} + + +void warn_fopen (void) +{ + { + FILE *q = fopen ("1", "r"); + release (q); // { dg-warning "'release' called on 'q' returned from a mismatched allocation function" } + fclose (q); + } + { + FILE *q = fdopen (0); + free (q); // { dg-warning "'free' called on 'q' returned from a mismatched allocation function" } + } + + { + FILE *q = fdopen (0); + q = realloc (q, 7); // { dg-warning "'realloc' called on 'q' returned from a mismatched allocation function" } + sink (q); + } +} + + +void test_popen (void) +{ + { + FILE *p = popen ("1", "r"); + sink (p); + pclose (p); + } + + { + FILE *p; + p = popen ("2", "r"); // { dg-message "allocated here" } + fclose (p); // { dg-warning "'fclose' called on 'p' returned from a mismatched allocation function" } + } + + { + /* freopen() can close a stream open by popen() but pclose() can't + close the stream returned from freopen(). */ + FILE *p = popen ("2", "r"); + p = freopen ("3", "r", p); // { dg-message "allocated here" } + pclose (p); // { dg-warning "'pclose' called on 'p' returned from a mismatched allocation function" } + } +} + + +void test_tmpfile (void) +{ + { + FILE *p = tmpfile (); + fclose (p); + } + + { + FILE *p = tmpfile (); + p = freopen ("1", "r", p); + fclose (p); + } + + { + FILE *p = tmpfile (); // { dg-message "allocated here" } + pclose (p); // { dg-warning "'pclose' called on 'p' returned from a mismatched allocation function" } + } +} + + +void warn_malloc (void) +{ + { + FILE *p = malloc (100); // { dg-message "allocated here" } + fclose (p); // { dg-warning "'p' should have been deallocated with 'free' but was deallocated with 'fclose'" } + } + + { + FILE *p = malloc (100); // { dg-message "allocated here" } + p = freopen ("1", "r", p);// { dg-warning "'p' should have been deallocated with 'free' but was deallocated with 'freopen'" } + fclose (p); + } + + { + FILE *p = malloc (100); // { dg-message "allocated here" } + pclose (p); // { dg-warning "'p' should have been deallocated with 'free' but was deallocated with 'pclose'" } + } +} + + +void test_acquire (void) +{ + { + FILE *p = acquire (); + release (p); + } + + { + FILE *p = acquire (); + release (p); + } + + { + FILE *p = acquire (); // { dg-message "allocated here \\(expects deallocation with 'release'\\)" } + fclose (p); // { dg-warning "'p' should have been deallocated with 'release' but was deallocated with 'fclose'" } + } + + { + FILE *p = acquire (); // { dg-message "allocated here \\(expects deallocation with 'release'\\)" } + pclose (p); // { dg-warning "'p' should have been deallocated with 'release' but was deallocated with 'pclose'" } + } + + { + FILE *p = acquire (); // { dg-message "allocated here \\(expects deallocation with 'release'\\)" } + p = freopen ("1", "r", p); // { dg-warning "'p' should have been deallocated with 'release' but was deallocated with 'freopen'" } + sink (p); + } + + { + FILE *p = acquire (); // { dg-message "allocated here \\(expects deallocation with 'release'\\)" } + free (p); // { dg-warning "'p' should have been deallocated with 'release' but was deallocated with 'free'" } + } + + { + FILE *p = acquire (); // { dg-message "allocated here \\(expects deallocation with 'release'\\)" } + p = realloc (p, 123); // { dg-warning "'p' should have been deallocated with 'release' but was deallocated with 'realloc'" } + sink (p); + } +} diff --git a/gcc/testsuite/gcc.dg/analyzer/attr-malloc-CVE-2019-19078-usb-leak.c b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-CVE-2019-19078-usb-leak.c new file mode 100644 index 00000000000..905d50ec3f9 --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-CVE-2019-19078-usb-leak.c @@ -0,0 +1,224 @@ +/* Adapted from linux 5.3.11: drivers/net/wireless/ath/ath10k/usb.c + Reduced reproducer for CVE-2019-19078 (leak of struct urb). */ + +typedef unsigned char u8; +typedef unsigned short u16; +typedef _Bool bool; + +#define ENOMEM 12 +#define EINVAL 22 + +/* The original file has this licence header. */ + +// SPDX-License-Identifier: ISC +/* + * Copyright (c) 2007-2011 Atheros Communications Inc. + * Copyright (c) 2011-2012,2017 Qualcomm Atheros, Inc. + * Copyright (c) 2016-2017 Erik Stromdahl + */ + +/* Adapted from include/linux/compiler_attributes.h. */ +#define __aligned(x) __attribute__((__aligned__(x))) +#define __printf(a, b) __attribute__((__format__(printf, a, b))) + +/* Possible macro for the new attribute. */ +#define __malloc(f) __attribute__((malloc(f))); + +/* From include/linux/types.h. */ + +typedef unsigned int gfp_t; + +/* Not the real value, which is in include/linux/gfp.h. */ +#define GFP_ATOMIC 32 + +/* From include/linux/usb.h. */ + +struct urb; +extern void usb_free_urb(struct urb *urb); +extern struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags) + __malloc(usb_free_urb); +/* attribute added as part of testcase */ + +extern int usb_submit_urb(/*struct urb *urb, */gfp_t mem_flags); +extern void usb_unanchor_urb(struct urb *urb); + +/* From drivers/net/wireless/ath/ath10k/core.h. */ + +struct ath10k; + +struct ath10k { + /* [...many other fields removed...] */ + + /* must be last */ + u8 drv_priv[0] __aligned(sizeof(void *)); +}; + +/* From drivers/net/wireless/ath/ath10k/debug.h. */ + +enum ath10k_debug_mask { + /* [...other values removed...] */ + ATH10K_DBG_USB_BULK = 0x00080000, +}; + +extern unsigned int ath10k_debug_mask; + +__printf(3, 4) void __ath10k_dbg(struct ath10k *ar, + enum ath10k_debug_mask mask, + const char *fmt, ...); + +/* Simplified for now, to avoid pulling in tracepoint code. */ +static inline +bool trace_ath10k_log_dbg_enabled(void) { return 0; } + +#define ath10k_dbg(ar, dbg_mask, fmt, ...) \ +do { \ + if ((ath10k_debug_mask & dbg_mask) || \ + trace_ath10k_log_dbg_enabled()) \ + __ath10k_dbg(ar, dbg_mask, fmt, ##__VA_ARGS__); \ +} while (0) + +/* From drivers/net/wireless/ath/ath10k/hif.h. */ + +struct ath10k_hif_sg_item { + /* [...other fields removed...] */ + void *transfer_context; /* NULL = tx completion callback not called */ +}; + +struct ath10k_hif_ops { + /* send a scatter-gather list to the target */ + int (*tx_sg)(struct ath10k *ar, u8 pipe_id, + struct ath10k_hif_sg_item *items, int n_items); + /* [...other fields removed...] */ +}; + +/* From drivers/net/wireless/ath/ath10k/usb.h. */ + +/* tx/rx pipes for usb */ +enum ath10k_usb_pipe_id { + /* [...other values removed...] */ + ATH10K_USB_PIPE_MAX = 8 +}; + +struct ath10k_usb_pipe { + /* [...all fields removed...] */ +}; + +/* usb device object */ +struct ath10k_usb { + /* [...other fields removed...] */ + struct ath10k_usb_pipe pipes[ATH10K_USB_PIPE_MAX]; +}; + +/* usb urb object */ +struct ath10k_urb_context { + /* [...other fields removed...] */ + struct ath10k_usb_pipe *pipe; + struct sk_buff *skb; +}; + +static inline struct ath10k_usb *ath10k_usb_priv(struct ath10k *ar) +{ + return (struct ath10k_usb *)ar->drv_priv; +} + +/* The source file. */ + +static void ath10k_usb_post_recv_transfers(struct ath10k *ar, + struct ath10k_usb_pipe *recv_pipe); + +struct ath10k_urb_context * +ath10k_usb_alloc_urb_from_pipe(struct ath10k_usb_pipe *pipe); + +void ath10k_usb_free_urb_to_pipe(struct ath10k_usb_pipe *pipe, + struct ath10k_urb_context *urb_context); + +static int ath10k_usb_hif_tx_sg(struct ath10k *ar, u8 pipe_id, + struct ath10k_hif_sg_item *items, int n_items) +{ + struct ath10k_usb *ar_usb = ath10k_usb_priv(ar); + struct ath10k_usb_pipe *pipe = &ar_usb->pipes[pipe_id]; + struct ath10k_urb_context *urb_context; + struct sk_buff *skb; + struct urb *urb; + int ret, i; + + for (i = 0; i < n_items; i++) { + urb_context = ath10k_usb_alloc_urb_from_pipe(pipe); + if (!urb_context) { + ret = -ENOMEM; + goto err; + } + + skb = items[i].transfer_context; + urb_context->skb = skb; + + urb = usb_alloc_urb(0, GFP_ATOMIC); /* { dg-message "allocated here" } */ + if (!urb) { + ret = -ENOMEM; + goto err_free_urb_to_pipe; + } + + /* TODO: these are disabled, otherwise we conservatively + assume that they could free urb. */ +#if 0 + usb_fill_bulk_urb(urb, + ar_usb->udev, + pipe->usb_pipe_handle, + skb->data, + skb->len, + ath10k_usb_transmit_complete, urb_context); + if (!(skb->len % pipe->max_packet_size)) { + /* hit a max packet boundary on this pipe */ + urb->transfer_flags |= URB_ZERO_PACKET; + } + + usb_anchor_urb(urb, &pipe->urb_submitted); +#endif + /* TODO: initial argument disabled, otherwise we conservatively + assume that it could free urb. */ + ret = usb_submit_urb(/*urb, */GFP_ATOMIC); + if (ret) { /* TODO: why doesn't it show this condition at default verbosity? */ + ath10k_dbg(ar, ATH10K_DBG_USB_BULK, + "usb bulk transmit failed: %d\n", ret); + + /* TODO: this is disabled, otherwise we conservatively + assume that it could free urb. */ +#if 0 + usb_unanchor_urb(urb); +#endif + + ret = -EINVAL; + /* Leak of urb happens here. */ + goto err_free_urb_to_pipe; + } + + /* TODO: the loop confuses the double-free checker (another + instance of PR analyzer/93695). */ + usb_free_urb(urb); /* { dg-bogus "double-'usb_free_urb' of 'urb'" "" { xfail *-*-* } } */ + } + + return 0; + +err_free_urb_to_pipe: + ath10k_usb_free_urb_to_pipe(urb_context->pipe, urb_context); +err: + return ret; /* { dg-warning "leak of 'urb'" } */ +} + +static const struct ath10k_hif_ops ath10k_usb_hif_ops = { + .tx_sg = ath10k_usb_hif_tx_sg, +}; + +/* Simulate code to register the callback. */ +extern void callback_registration (const void *); +int ath10k_usb_probe(void) +{ + callback_registration(&ath10k_usb_hif_ops); +} + + +/* The original source file ends with: +MODULE_AUTHOR("Atheros Communications, Inc."); +MODULE_DESCRIPTION("Driver support for Qualcomm Atheros 802.11ac WLAN USB devices"); +MODULE_LICENSE("Dual BSD/GPL"); +*/ diff --git a/gcc/testsuite/gcc.dg/analyzer/attr-malloc-misuses.c b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-misuses.c new file mode 100644 index 00000000000..3c6c17bddde --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-malloc-misuses.c @@ -0,0 +1,18 @@ +extern void free (void *); + +int not_a_fn __attribute__ ((malloc (free))); /* { dg-warning "'malloc' attribute ignored; valid only for functions" } */ + +void void_return (void) __attribute__ ((malloc(free))); /* { dg-warning "'malloc' attribute ignored on functions returning 'void'" } */ + +void test_void_return (void) +{ + void_return (); +} + +extern void void_args (void); /* { dg-message "declared here" } */ +void *has_malloc_with_void_args (void) + __attribute__ ((malloc(void_args))); /* { dg-error "'malloc' attribute argument 1 must take a pointer type as its first argument; have 'void'" } */ + +extern void no_args (); /* { dg-message "declared here" } */ +void *has_malloc_with_no_args (void) + __attribute__ ((malloc(no_args))); /* { dg-error "'malloc' attribute argument 1 must take a pointer type as its first argument" } */ -- 2.30.2