This work-in-progress patch generalizes the malloc/free problem-checking in -fanalyzer so that it can work on arbitrary acquire/release API pairs.
It adds a new __attribute__((deallocated_by(FOO))) that could be used like this in a library header: struct foo; extern void foo_release (struct foo *); extern struct foo *foo_acquire (void) __attribute__ ((deallocated_by(foo_release))); In theory, the analyzer then "knows" these functions are an acquire/release pair, and can emit diagnostics for leaks, double-frees, use-after-frees, mismatching deallocations, etc. 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. So I don't know how much use this feature would be as-is. (without either requiring lots of additional attributes for marking fndecl args as being merely borrowed, or simply assuming that they are borrowed in the absence of a function body to analyze) Thoughts? Dave gcc/analyzer/ChangeLog: * 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". (enum wording): Add WORDING_DEALLOCATED. (malloc_state_machine::custom_api_map_t): New typedef. (malloc_state_machine::m_custom_apis): New field. (start_p): New. (use_after_free::describe_state_change): Handle WORDING_DEALLOCATED. (use_after_free::describe_final_event): Likewise. (malloc_leak::describe_state_change): Only emit "allocated here" on a start->nonnull transition, rather than on other transitions to nonnull. (malloc_state_machine::~malloc_state_machine): New. (malloc_state_machine::on_stmt): Handle "__attribute__((deallocated_by(FOO)))", and the special attribute set on FOO. (malloc_state_machine::get_or_create_api): New. (malloc_state_machine::on_allocator_call): Add "returns_nonnull" param and use it to affect which state to transition to. gcc/c-family/ChangeLog: * c-attribs.c (c_common_attribute_table): Add entry for "deallocated_by". (matching_deallocator_type_p): New. (maybe_add_deallocator_attribute): New. (handle_deallocated_by_attribute): New. gcc/ChangeLog: * doc/extend.texi (Common Function Attributes): Add "deallocated_by". gcc/testsuite/ChangeLog: * gcc.dg/analyzer/attr-deallocated_by-1.c: New test. * gcc.dg/analyzer/attr-deallocated_by-1a.c: New test. * gcc.dg/analyzer/attr-deallocated_by-2.c: New test. * gcc.dg/analyzer/attr-deallocated_by-3.c: New test. * gcc.dg/analyzer/attr-deallocated_by-4.c: New test. * gcc.dg/analyzer/attr-deallocated_by-CVE-2019-19078-usb-leak.c: New test. * gcc.dg/analyzer/attr-deallocated_by-misuses.c: New test. --- gcc/analyzer/region-model-impl-calls.cc | 9 + gcc/analyzer/region-model.cc | 10 + gcc/analyzer/region-model.h | 1 + gcc/analyzer/sm-malloc.cc | 107 ++++++++- gcc/c-family/c-attribs.c | 116 ++++++++++ gcc/doc/extend.texi | 89 ++++++++ .../gcc.dg/analyzer/attr-deallocated_by-1.c | 74 +++++++ .../gcc.dg/analyzer/attr-deallocated_by-1a.c | 69 ++++++ .../gcc.dg/analyzer/attr-deallocated_by-2.c | 24 ++ .../gcc.dg/analyzer/attr-deallocated_by-3.c | 31 +++ .../gcc.dg/analyzer/attr-deallocated_by-4.c | 21 ++ ...r-deallocated_by-CVE-2019-19078-usb-leak.c | 208 ++++++++++++++++++ .../analyzer/attr-deallocated_by-misuses.c | 26 +++ 13 files changed, 779 insertions(+), 6 deletions(-) create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-1.c create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-1a.c create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-2.c create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-3.c create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-4.c create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-CVE-2019-19078-usb-leak.c create mode 100644 gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-misuses.c diff --git a/gcc/analyzer/region-model-impl-calls.cc b/gcc/analyzer/region-model-impl-calls.cc index 009b8c3ecb0..5fcbbbc88bf 100644 --- a/gcc/analyzer/region-model-impl-calls.cc +++ b/gcc/analyzer/region-model-impl-calls.cc @@ -430,6 +430,15 @@ region_model::impl_call_strlen (const call_details &cd) return true; } +/* Handle calls to functions referenced by + __attribute__((deallocated_by(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 a88a295a241..b243051d112 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 @@ -815,6 +816,15 @@ region_model::on_call_post (const gcall *call, impl_call_operator_delete (cd); return; } + /* Was this fndecl referenced by + __attribute__((deallocated_by(FOO)))? */ + if (lookup_attribute ("arg of deallocated_by", + 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 cfeac8d6951..ec283ec29e9 100644 --- a/gcc/analyzer/region-model.h +++ b/gcc/analyzer/region-model.h @@ -2577,6 +2577,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 6293d7885cd..e7f803bc663 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 @@ -109,7 +111,8 @@ 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 @@ -167,6 +170,7 @@ 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); @@ -218,6 +222,11 @@ public: api m_scalar_new; api m_vector_new; + /* Support for "__attribute__((deallocated_by(FOO)))". + Map from deallocator fndecl to api. */ + typedef hash_map<tree, api *> custom_api_map_t; + custom_api_map_t m_custom_apis; + /* States that are independent of api. */ /* State for a pointer that's known to be NULL. */ @@ -232,9 +241,12 @@ public: state_t m_stop; private: + const api &get_or_create_api (tree deallocator_fndecl); + void on_allocator_call (sm_context *sm_ctxt, const gcall *call, - const api &ap) const; + const api &ap, + bool returns_nonnull = false) const; void on_deallocator_call (sm_context *sm_ctxt, const supernode *node, const gcall *call, @@ -291,6 +303,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 @@ -762,6 +782,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); @@ -781,6 +803,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", @@ -815,7 +841,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"); @@ -964,6 +991,13 @@ malloc_state_machine::malloc_state_machine (logger *logger) m_stop = add_state ("stop", RS_STOP, NULL); } +malloc_state_machine::~malloc_state_machine () +{ + for (custom_api_map_t::iterator iter = m_custom_apis.begin (); + iter != m_custom_apis.end (); ++iter) + delete (*iter).second; +} + state_machine::state_t malloc_state_machine::add_state (const char *name, enum resource_state rs, const api *a) @@ -1028,6 +1062,29 @@ malloc_state_machine::on_stmt (sm_context *sm_ctxt, return true; } + /* Handle "__attribute__((deallocated_by(FOO)))". */ + if (tree attr = lookup_attribute ("deallocated_by", + DECL_ATTRIBUTES (callee_fndecl))) + { + tree list = TREE_VALUE (attr); + gcc_assert (TREE_CODE (list) == TREE_LIST); + tree deallocator_fndecl = TREE_VALUE (list); + gcc_assert (TREE_CODE (deallocator_fndecl) == FUNCTION_DECL); + + malloc_state_machine *nonconst + = const_cast <malloc_state_machine *> (this); + const api &a = nonconst->get_or_create_api (deallocator_fndecl); + + /* FIXME: deallocated_by is an attribute on the fndecl, whereas + returns_nonnull is an attribute on the function type. Should + deallocated_by also be on the function type? */ + bool returns_nonnull + = lookup_attribute ("returns_nonnull", + TYPE_ATTRIBUTES (TREE_TYPE (callee_fndecl))); + + on_allocator_call (sm_ctxt, call, a, returns_nonnull); + } + /* Handle "__attribute__((nonnull))". */ { tree fntype = TREE_TYPE (callee_fndecl); @@ -1070,6 +1127,17 @@ 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". */ + if (lookup_attribute ("arg of deallocated_by", + DECL_ATTRIBUTES (callee_fndecl))) + { + malloc_state_machine *nonconst + = const_cast <malloc_state_machine *> (this); + const api &a = nonconst->get_or_create_api (callee_fndecl); + on_deallocator_call (sm_ctxt, node, call, a); + } } if (tree lhs = sm_ctxt->is_zero_assignment (stmt)) @@ -1137,18 +1205,45 @@ malloc_state_machine::on_stmt (sm_context *sm_ctxt, return false; } -/* Handle a call to an allocator. */ +/* Get the api for DEALLOCATOR_FNDECL, creating it if it doesn't + already exist. */ + +const api & +malloc_state_machine::get_or_create_api (tree deallocator_fndecl) +{ + if (fndecl_built_in_p (deallocator_fndecl, BUILT_IN_FREE)) + return m_malloc; + + if (api **slot = m_custom_apis.get (deallocator_fndecl)) + return **slot; + + const char *deallocator_funcname + = IDENTIFIER_POINTER (DECL_NAME (deallocator_fndecl)); + api *a = new api (this, + deallocator_funcname, + deallocator_funcname, + WORDING_DEALLOCATED); + m_custom_apis.put (deallocator_fndecl, a); + return *a; +} + +/* 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 api &ap, + 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 + ? ap.m_nonnull : ap.m_unchecked)); } else { diff --git a/gcc/c-family/c-attribs.c b/gcc/c-family/c-attribs.c index c779d13f023..ce2a958a1bc 100644 --- a/gcc/c-family/c-attribs.c +++ b/gcc/c-family/c-attribs.c @@ -127,6 +127,7 @@ static tree handle_cleanup_attribute (tree *, tree, tree, int, bool *); static tree handle_warn_unused_result_attribute (tree *, tree, tree, int, bool *); static tree handle_access_attribute (tree *, tree, tree, int, bool *); +static tree handle_deallocated_by_attribute (tree *, tree, tree, int, bool *); static tree handle_sentinel_attribute (tree *, tree, tree, int, bool *); static tree handle_type_generic_attribute (tree *, tree, tree, int, bool *); @@ -491,6 +492,8 @@ const struct attribute_spec c_common_attribute_table[] = handle_noinit_attribute, attr_noinit_exclusions }, { "access", 1, 3, false, true, true, false, handle_access_attribute, NULL }, + { "deallocated_by", 1, 1, true, false, false, false, + handle_deallocated_by_attribute, NULL}, { NULL, 0, 0, false, false, false, false, NULL, NULL } }; @@ -4663,6 +4666,119 @@ build_attr_access_from_parms (tree parms, bool skip_voidptr) return tree_cons (name, attrargs, NULL_TREE); } +/* Return true if DEALLOCATOR_ARG_TYPE is a suitable type for the + initial argument of fndecl in __attribute((deallocated_by (fndecl) )) + when the attribute is applied to a function returning + ALLOCATOR_RETURN_TYPE. */ + +static bool +matching_deallocator_type_p (const_tree deallocator_arg_type, + const_tree allocator_return_type) +{ + /* It's OK if the deallocator is "void *" and the allocator returns + a pointer. */ + if (deallocator_arg_type == ptr_type_node + && POINTER_TYPE_P (allocator_return_type)) + return true; + + /* Check for exact match. */ + if (deallocator_arg_type == allocator_return_type) + return true; + + /* TODO: do we care about C-style "inheritance" such as in GTK + and CPython? */ + + return false; +} + +/* Subroutine of handle_deallocated_by_attribute. + Idempotently add a special attribute to the fndecl referenced + by __attribute__ ((deallocated_by(FOO))), marking it as such. */ + +static void +maybe_add_deallocator_attribute (tree deallocator_decl) +{ + /* This special attribute name has spaces in it, so it can't + be added via the normal syntax. */ + const char *attr_name_str = "arg of deallocated_by"; + tree old_attrs = DECL_ATTRIBUTES (deallocator_decl); + + /* Idempotency. */ + if (old_attrs) + if (lookup_attribute (attr_name_str, old_attrs)) + return; + + tree attr_name = get_identifier (attr_name_str); + tree new_attrs = tree_cons (attr_name, NULL_TREE, old_attrs); + + DECL_ATTRIBUTES (deallocator_decl) = new_attrs; +} + +/* Handle the "deallocated_by" attribute (fndecl). */ + +static tree +handle_deallocated_by_attribute (tree *node, tree name, tree args, + int ARG_UNUSED (flags), bool *no_add_attrs) +{ + if (TREE_CODE (*node) != FUNCTION_DECL) + { + if (warning (OPT_Wattributes, "%qE attribute ignored", name)) + inform (input_location, "%qE is only usable on function declarations", + name); + return NULL_TREE; + } + + const_tree allocator_fndecl_return_type = TREE_TYPE (TREE_TYPE ((*node))); + + if (VOID_TYPE_P (allocator_fndecl_return_type)) + { + error ("%qE attribute applied to function with %<void%> return type", + name); + *no_add_attrs = true; + return NULL_TREE; + } + + tree deallocator_decl = TREE_VALUE (args); + if (TREE_CODE (deallocator_decl) != FUNCTION_DECL) + { + error ("%qE argument not a function", name); + *no_add_attrs = true; + return NULL_TREE; + } + + /* Check the type of deallocator function. + It must have at least one argument; the first argument must have + the same type as the return type of fndecl we're applying the + attribute to. */ + const_tree dealloc_arg_types = TYPE_ARG_TYPES (TREE_TYPE (deallocator_decl)); + if (dealloc_arg_types == NULL_TREE + || VOID_TYPE_P (TREE_VALUE (dealloc_arg_types))) + { + error ("%qE argument must have at least one argument", name); + inform (DECL_SOURCE_LOCATION (deallocator_decl), "declared here"); + *no_add_attrs = true; + return NULL_TREE; + } + const tree deallocator_first_arg_type = TREE_VALUE (dealloc_arg_types); + if (!matching_deallocator_type_p (deallocator_first_arg_type, + allocator_fndecl_return_type)) + { + error ("%qE attribute applied to function returning type %qT...", + name, allocator_fndecl_return_type); + inform (DECL_SOURCE_LOCATION (deallocator_decl), + "...whereas %qD accepts type %qT", + deallocator_decl, deallocator_first_arg_type); + *no_add_attrs = true; + return NULL_TREE; + } + + /* The attribute should also affect the deallocator fndecl. + Add a special attribute to mark it as such. */ + maybe_add_deallocator_attribute (deallocator_decl); + + return NULL_TREE; +} + /* Handle a "nothrow" attribute; arguments as in struct attribute_spec.handler. */ diff --git a/gcc/doc/extend.texi b/gcc/doc/extend.texi index b9684dc7a06..cc4f26e9ab0 100644 --- a/gcc/doc/extend.texi +++ b/gcc/doc/extend.texi @@ -2835,6 +2835,95 @@ extern __attribute__ ((alloc_size (1), malloc, nothrow)) StrongAlias (allocate, alloc); @end smallexample +@item deallocated_by (@var{function}) +@cindex @code{deallocated_by} function attribute +The @code{deallocated_by} attribute indicates that the function it applies +to is an allocator function, and that the result of calls to the function +ought to be deallocated by @var{function}. This affects how +@option{-fanalyzer} handles both functions: the function with the attribute +is treated as an allocator, and @var{function} is treated as a deallocator. +The attribute can only be applied to functions that return a non-void +result. The first argument of the deallocator function must +have a type matching the result of the allocator function. Alternatively, +if both are pointers, the deallocator function can accept a ``void *''. + +For example, in the following + +@smallexample +extern void foo_release (struct foo *); +extern struct foo *foo_acquire (void) + __attribute__ ((deallocated_by(foo_release))); +@end smallexample + +the function ``foo_acquire'' is marked as being an allocator, with the +resulting ``struct foo *'' needing to be deallocated with a call to +``foo_release''. + +A deallocation function can be shared between more than one allocation +function - for example, many functions could be marked +@code{__attribute__ ((deallocated_by(free)))}. + +This attribute is used by @option{-fanalyzer} to enable various warnings: + +@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 assume that the 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, returns_nonnull, deallocated_by(free))); +@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 the deallocation function is used on a global or on-stack variable. + +@end itemize + +The @code{deallocated_by} attribute differs from the @code{malloc} +attribute in that the former marks an allocation/deallocation pair, +whereas the latter gives hints about pointer aliasing to the optimizer. +It may make sense to mark a function declaration with both attributes, +such as: + +@smallexample +extern void foo_release (struct foo *); +extern struct foo *foo_acquire (void) + __attribute__ ((malloc, deallocated_by(foo_release))); +@end smallexample + +It is assumed that the deallocator can gracefully handle the 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 deprecated @itemx deprecated (@var{msg}) @cindex @code{deprecated} function attribute diff --git a/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-1.c b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-1.c new file mode 100644 index 00000000000..61fb0260136 --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-1.c @@ -0,0 +1,74 @@ +extern void free (void *); + +struct foo +{ + int m_int; +}; + +extern void foo_release (struct foo *); +extern struct foo *foo_acquire (void) + __attribute__ ((malloc, deallocated_by(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" } */ +} + +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-deallocated_by-1a.c b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-1a.c new file mode 100644 index 00000000000..14ea7d3f9d5 --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-1a.c @@ -0,0 +1,69 @@ +/* As attr-deallocated_by-1a.c, but without "malloc" attribute on foo_acquire. */ + +extern void free (void *); + +struct foo +{ + int m_int; +}; + +extern void foo_release (struct foo *); +extern struct foo *foo_acquire (void) + __attribute__ ((deallocated_by(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_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" } */ +} + +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-deallocated_by-2.c b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-2.c new file mode 100644 index 00000000000..2a669d99d36 --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-2.c @@ -0,0 +1,24 @@ +extern void free (void *); +char *xstrdup (const char *) + __attribute__((malloc, returns_nonnull, deallocated_by(free))); + +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-deallocated_by-3.c b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-3.c new file mode 100644 index 00000000000..ef615531e17 --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-3.c @@ -0,0 +1,31 @@ +/* A non-pointer resource. */ + +typedef int handle_t; + +extern void handle_release (handle_t h); +extern handle_t handle_acquire (void) + __attribute__ ((deallocated_by(handle_release))); +extern void unknown_fn (handle_t h); + +void test_1 (void) +{ + handle_t h = handle_acquire (); + handle_release (h); +} + +void test_2 (void) +{ + handle_t h = handle_acquire (); /* { dg-message "allocated here" } */ +} /* { dg-warning "leak of 'h'" } */ + +void test_3 (handle_t h) +{ + handle_release (h); + handle_release (h); /* { dg-warning "double-'handle_release' of 'h'" } */ +} + +void test_4 (void) +{ + handle_t h = handle_acquire (); + unknown_fn (h); +} diff --git a/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-4.c b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-4.c new file mode 100644 index 00000000000..a62f45bd571 --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-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, deallocated_by(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-deallocated_by-CVE-2019-19078-usb-leak.c b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-CVE-2019-19078-usb-leak.c new file mode 100644 index 00000000000..a008c50ad50 --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-CVE-2019-19078-usb-leak.c @@ -0,0 +1,208 @@ +/* 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 <erik.stromd...@gmail.com> + */ + +/* 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 __deallocated_by(f) __attribute__((deallocated_by(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) + __deallocated_by(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 */ +}; + +/* 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); + +/* TODO: this was static and a callback; if made static it's never analyzed + (PR analyzer/97258). */ + +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'" } */ +} + +/* 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-deallocated_by-misuses.c b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-misuses.c new file mode 100644 index 00000000000..ddbda0d2161 --- /dev/null +++ b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-misuses.c @@ -0,0 +1,26 @@ +extern void free (void *); + +int not_a_fn __attribute__ ((deallocated_by(free))); /* { dg-warning "'deallocated_by' attribute ignored" } */ +/* { dg-message "'deallocated_by' is only usable on function declarations" "" { target *-*-* } .-1 } */ + +void void_return (void) __attribute__ ((deallocated_by(free))); /* { dg-error "'deallocated_by' attribute applied to function with 'void' return type" } */ + +void test_void_return (void) +{ + void_return (); +} + +extern void void_args (void); /* { dg-message "declared here" } */ +void *has_deallocated_by_with_void_args (void) + __attribute__ ((deallocated_by(void_args))); /* { dg-error "'deallocated_by' argument must have at least one argument" } */ + +extern void no_args (); /* { dg-message "declared here" } */ +void *has_deallocated_by_with_no_args (void) + __attribute__ ((deallocated_by(no_args))); /* { dg-error "'deallocated_by' argument must have at least one argument" } */ + +/* Mismatching types. */ +struct foo {}; +struct bar {}; +extern void takes_foo (struct foo *); /* { dg-message "\\.\\.\\.whereas 'takes_foo' accepts type 'struct foo \\*'" } */ +struct bar *wrong_deallocator_type (void) + __attribute__ ((deallocated_by(takes_foo))); /* { dg-error "'deallocated_by' attribute applied to function returning type 'struct bar \\*'\\.\\.\\." } */ -- 2.26.2