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

Reply via email to