From 37807c6b5199ebe13efb6716f922a18ce67f1e5f Mon Sep 17 00:00:00 2001
From: David Malcolm <dmalcolm@redhat.com>
Date: Tue, 8 Sep 2020 11:45:39 -0400
Subject: [PATCH 29/47] FIXME: WIP on __attribute__((deallocated_by(FOO)))

---
 gcc/analyzer/sm-malloc.cc                     | 103 ++++++++-
 gcc/c-family/c-attribs.c                      |  95 +++++++-
 gcc/doc/extend.texi                           |  84 +++++++
 .../gcc.dg/analyzer/attr-deallocated_by-1.c   |  68 ++++++
 .../gcc.dg/analyzer/attr-deallocated_by-1a.c  |  57 +++++
 .../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 | 206 ++++++++++++++++++
 .../analyzer/attr-deallocated_by-misuses.c    |  26 +++
 10 files changed, 708 insertions(+), 7 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/sm-malloc.cc b/gcc/analyzer/sm-malloc.cc
index 6293d7885cd..4aba48e6c14 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,14 @@ 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 +1063,35 @@ 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);
+	  }
+	// TODO: also need to handle the deallocator calls
+	/* TODO: do we need to also handle the deallocator calls in
+	   region-model.cc, like "free".  If so, we may need to move this
+	   attribute-handling out of here, or to extend it.
+	   (currently they're being treated as unknown fncalls).  */
+	// TODO: add documentation
+
 	/* Handle "__attribute__((nonnull))".   */
 	{
 	  tree fntype = TREE_TYPE (callee_fndecl);
@@ -1137,18 +1201,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 70b00037d98..01fe786a276 100644
--- a/gcc/c-family/c-attribs.c
+++ b/gcc/c-family/c-attribs.c
@@ -127,7 +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 *);
 static tree handle_alloc_size_attribute (tree *, tree, tree, int, bool *);
@@ -491,6 +491,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 }
 };
 
@@ -4669,6 +4671,97 @@ 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;
+  return false;
+}
+
+/* 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.
+     TODO: but what about C-style "inheritance"?  */
+  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;
+    }
+
+#if 0
+  // TODO: register the deallocator function
+  /* TODO: would this be better handled by simply looking for the
+     attribute at each call?  */
+  if (flag_analyzer)
+    ana::register_deallocator (*node, deallocator_decl);
+#endif
+
+  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 c9f72991bba..8dbab8d258b 100644
--- a/gcc/doc/extend.texi
+++ b/gcc/doc/extend.texi
@@ -2835,6 +2835,90 @@ 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.
+
+@gol TODO: -Wanalyzer-double-free
+
+@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
+
+@gol TODO: -Wanalyzer-use-after-free
+
+@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.
+@gol TODO: this doesn't work yet
+
+@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..37a4276a396
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-1.c
@@ -0,0 +1,68 @@
+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); // TODO: double-release warning!
+}
+
+void test_4a (void)
+{
+  struct foo *p = foo_acquire ();
+  foo_release (p);
+  foo_release (p); // TODO: double-release warning!
+}
+
+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); // TODO: free-of-non-heap warning!
+}
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..cb0f37f5755
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-1a.c
@@ -0,0 +1,57 @@
+/* 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); // TODO: double-release warning!
+}
+
+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!
+}
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..fada84e747c
--- /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); // TODO: double-release warning!
+}
+
+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..530610d5980
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/attr-deallocated_by-CVE-2019-19078-usb-leak.c
@@ -0,0 +1,206 @@
+/* Adapted from linux 5.3.11: drivers/net/wireless/ath/ath10k/usb.c
+   Reduced reproducer for CVE-2019-19078 (leak of struct usb).  */
+
+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.stromdahl@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;
+		}
+
+		usb_free_urb(urb);
+	}
+
+	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

