Bug 94527 is request from the kernel developers for an attribute to indicate that a user-defined function deallocates an object allocated by an earlier call to an allocation function. Their goal is to detect misuses of such functions and the pointers or objects returned from them.
The recently submitted patches(*) enable the detection of a subset of such misuses for standard allocation functions like malloc and free, but those are just a small fraction of allocation/deallocation functions used in practice, and only rarely used in the kernel (mostly in utility programs). The attached patch extends attribute malloc to enable this detection also for user-defined functions. The design extends attribute malloc to accept one or two optional arguments: one naming a deallocation function that deallocates pointers returned from the malloc-like function, and another to denote the position of the pointer argument in the deallocation functions parameter list. Any number of deallocators can be associated with any number of allocators. This makes it possible to annotate, for example, all the POSIX <stdio.h> functions that open and close FILE streams and detect mismatches between any pairs that aren't suitable (in addition to calling free on a FILE* returned from fopen, for instance). An association with an allocator results in adding an internal "*dealloc" attribute to the deallocator so that the former can be quickly looked up based on a call to the latter. Tested on x86_64-linux + Glibc & Binutils/GDB (no instances of the new warnings). Martin [*] Prerequisite patch add -Wmismatched-new-delete to middle end (PR 90629) https://gcc.gnu.org/pipermail/gcc-patches/2020-November/557987.html PS In pr94527 Jonathan notes that failing to properly match pairs of calls isn't limited to APIs that return pointers and applies to other kinds of "handles" including integers (e.g., the POSIX open/close APIs), and a detection of such mismatches would be helpful as well. David submitted a prototype of this for the analyzer here: https://gcc.gnu.org/pipermail/gcc-patches/2020-October/555544.html I chose not to implement nonpointer detection for some of the same reasons as mentioned in comment #8 on the bug (and also because there's no support for it in the machinery I use). I also didn't use the same attribute as David, in part because I think it's better to provide separate attributes for pointer APIs and for others (integers), and in part also because the deallocated_by attribute design as is cannot accommodate my goal of supporting app standard functions (including the <stdio.h> freopen which "deallocates" the third argument).
PR middle-end/94527 - Add an __attribute__ that marks a function as freeing an object gcc/ChangeLog: PR middle-end/94527 * builtins.c (gimple_call_alloc_p): Handle user-defined functions. (fndecl_alloc_p): New helper. (call_dealloc_argno): New helper. (gimple_call_dealloc_p): Call it. (call_dealloc_p): Same. (matching_alloc_calls_p): Handle user-defined functions. (maybe_emit_free_warning): Same. * doc/extend.texi (attribute malloc): Update. * doc/invoke.texi (-Wmismatched-dealloc): Document new option. gcc/c-family/ChangeLog: PR middle-end/94527 * c-attribs.c (handle_dealloc_attribute): New function. (handle_malloc_attribute): Handle argument forms of attribute. * c.opt (-Wmismatched-dealloc): New option. (-Wmismatched-new-delete): Update description. gcc/testsuite/ChangeLog: PR middle-end/94527 * g++.dg/warn/Wmismatched-dealloc-2.C: New test. * g++.dg/warn/Wmismatched-dealloc.C: New test. * gcc.dg/Wmismatched-dealloc.c: New test. * gcc.dg/attr-malloc.c: New test. diff --git a/gcc/builtins.c b/gcc/builtins.c index ebdded69189..aad99da01c2 100644 --- a/gcc/builtins.c +++ b/gcc/builtins.c @@ -13014,10 +13014,9 @@ find_assignment_location (tree var) allocated objects. Otherwise return true even for all forms of alloca (including VLA). */ -bool -gimple_call_alloc_p (gimple *stmt, bool all_alloc /* = false */) +static bool +fndecl_alloc_p (tree fndecl, bool all_alloc /* = false */) { - tree fndecl = gimple_call_fndecl (stmt); if (!fndecl) return false; @@ -13025,24 +13024,53 @@ gimple_call_alloc_p (gimple *stmt, bool all_alloc /* = false */) if (DECL_IS_OPERATOR_NEW_P (fndecl)) return true; - /* TODO: Handle user-defined functions with attribute malloc. */ - if (!gimple_call_builtin_p (stmt, BUILT_IN_NORMAL)) + if (fndecl_built_in_p (fndecl, BUILT_IN_NORMAL)) + { + switch (DECL_FUNCTION_CODE (fndecl)) + { + case BUILT_IN_ALLOCA: + case BUILT_IN_ALLOCA_WITH_ALIGN: + return all_alloc; + case BUILT_IN_CALLOC: + case BUILT_IN_MALLOC: + case BUILT_IN_REALLOC: + case BUILT_IN_STRDUP: + case BUILT_IN_STRNDUP: + return true; + default: + break; + } + } + + /* A function is considered an allocation function if it's declared + with attribute malloc with an argument naming its associated + deallocation function. */ + tree attrs = DECL_ATTRIBUTES (fndecl); + if (!attrs) return false; - switch (DECL_FUNCTION_CODE (fndecl)) + for (tree allocs = attrs; + (allocs = lookup_attribute ("malloc", allocs)); + allocs = TREE_CHAIN (allocs)) { - case BUILT_IN_ALLOCA: - case BUILT_IN_ALLOCA_WITH_ALIGN: - return all_alloc; - case BUILT_IN_CALLOC: - case BUILT_IN_MALLOC: - case BUILT_IN_REALLOC: - case BUILT_IN_STRDUP: - case BUILT_IN_STRNDUP: - return true; - default: - return false; + tree args = TREE_VALUE (allocs); + if (!args) + continue; + + if (TREE_VALUE (args)) + return true; } + + return false; +} + +/* Return true if STMT is a call to an allocation function. A wrapper + around fndecl_alloc_p. */ + +bool +gimple_call_alloc_p (gimple *stmt, bool all_alloc /* = false */) +{ + return fndecl_alloc_p (gimple_call_fndecl (stmt), all_alloc); } /* Return the zero-based number corresponding to the argument being @@ -13050,9 +13078,9 @@ gimple_call_alloc_p (gimple *stmt, bool all_alloc /* = false */) if it isn't. */ static unsigned -gimple_call_dealloc_argno (gimple *stmt) +call_dealloc_argno (tree exp) { - tree fndecl = gimple_call_fndecl (stmt); + tree fndecl = get_callee_fndecl (exp); if (!fndecl) return UINT_MAX; @@ -13062,48 +13090,129 @@ gimple_call_dealloc_argno (gimple *stmt) /* TODO: Handle user-defined functions with attribute malloc? Handle known non-built-ins like fopen? */ - if (!gimple_call_builtin_p (stmt, BUILT_IN_NORMAL)) + if (fndecl_built_in_p (fndecl, BUILT_IN_NORMAL)) + { + switch (DECL_FUNCTION_CODE (fndecl)) + { + case BUILT_IN_FREE: + case BUILT_IN_REALLOC: + return 0; + default: + break; + } + return UINT_MAX; + } + + tree attrs = DECL_ATTRIBUTES (fndecl); + if (!attrs) return UINT_MAX; - switch (DECL_FUNCTION_CODE (fndecl)) + for (tree atfree = attrs; + (atfree = lookup_attribute ("*dealloc", atfree)); + atfree = TREE_CHAIN (atfree)) { - case BUILT_IN_FREE: - case BUILT_IN_REALLOC: - return 0; - default: - return UINT_MAX; + tree alloc = TREE_VALUE (atfree); + if (!alloc) + continue; + + tree pos = TREE_CHAIN (alloc); + if (!pos) + return 0; + + pos = TREE_VALUE (pos); + return TREE_INT_CST_LOW (pos) - 1; } + + return UINT_MAX; } /* Return true if STMT is a call to a deallocation function. */ static inline bool -gimple_call_dealloc_p (gimple *stmt) +call_dealloc_p (tree exp) { - return gimple_call_dealloc_argno (stmt) != UINT_MAX; + return call_dealloc_argno (exp) != UINT_MAX; } -/* Return true if DEALLOC_DECL is a function suitable to deallocate - objects allocated by calls to ALLOC_DECL. */ +/* ALLOC_DECL and DEALLOC_DECL are pair of allocation and deallocation + functions. Return true if the latter is suitable to deallocate objects + allocated by calls to the former. */ static bool matching_alloc_calls_p (tree alloc_decl, tree dealloc_decl) { if (DECL_IS_OPERATOR_NEW_P (alloc_decl)) { - if (!DECL_IS_OPERATOR_DELETE_P (dealloc_decl)) + if (DECL_IS_OPERATOR_DELETE_P (dealloc_decl)) + { + /* Return true iff both functions are of the same array or + singleton form and false otherwise. */ + tree alloc_id = DECL_NAME (alloc_decl); + tree dealloc_id = DECL_NAME (dealloc_decl); + const char *alloc_fname = IDENTIFIER_POINTER (alloc_id); + const char *dealloc_fname = IDENTIFIER_POINTER (dealloc_id); + return !strchr (alloc_fname, '[') == !strchr (dealloc_fname, '['); + } + + /* Return false for deallocation functions that are known not + to match. */ + if (fndecl_built_in_p (dealloc_decl, BUILT_IN_FREE) + || fndecl_built_in_p (dealloc_decl, BUILT_IN_REALLOC)) return false; + /* Otherwise proceed below to check the deallocation function's + "*dealloc" attributes to look for one that mentions this operator + new. */ + } + else if (fndecl_built_in_p (alloc_decl, BUILT_IN_NORMAL)) + { + switch (DECL_FUNCTION_CODE (alloc_decl)) + { + case BUILT_IN_ALLOCA: + case BUILT_IN_ALLOCA_WITH_ALIGN: + return false; + + case BUILT_IN_CALLOC: + case BUILT_IN_MALLOC: + case BUILT_IN_REALLOC: + case BUILT_IN_STRDUP: + case BUILT_IN_STRNDUP: + if (DECL_IS_OPERATOR_DELETE_P (dealloc_decl)) + return false; - /* Return true iff both functions are of the same array or - singleton form. */ - tree alloc_id = DECL_NAME (alloc_decl); - tree dealloc_id = DECL_NAME (dealloc_decl); - const char *alloc_fname = IDENTIFIER_POINTER (alloc_id); - const char *dealloc_fname = IDENTIFIER_POINTER (dealloc_id); - return !strchr (alloc_fname, '[') == !strchr (dealloc_fname, '['); + if (fndecl_built_in_p (dealloc_decl, BUILT_IN_FREE) + || fndecl_built_in_p (dealloc_decl, BUILT_IN_REALLOC)) + return true; + break; + + default: + break; + } + } + + /* If DEALLOC_DECL has internal "*dealloc" attribute scan the list of + its associated allocation functions for ALLOC_DECL. If it's found + they are a matching pair, otherwise they're not. */ + tree attrs = DECL_ATTRIBUTES (dealloc_decl); + if (!attrs) + return false; + + for (tree funs = attrs; + (funs = lookup_attribute ("*dealloc", funs)); + funs = TREE_CHAIN (funs)) + { + tree args = TREE_VALUE (funs); + if (!args) + continue; + + tree fname = TREE_VALUE (args); + if (!fname) + continue; + + if (fname == DECL_NAME (alloc_decl)) + return true; } - return !DECL_IS_OPERATOR_DELETE_P (dealloc_decl); + return false; } /* Return true if DEALLOC_DECL is a function suitable to deallocate @@ -13175,24 +13284,19 @@ maybe_emit_free_warning (tree exp) if (!fndecl) return; - if (!fndecl_built_in_p (fndecl, BUILT_IN_FREE) - && !fndecl_built_in_p (fndecl, BUILT_IN_REALLOC) - && !DECL_IS_OPERATOR_DELETE_P (fndecl)) + unsigned argno = call_dealloc_argno (exp); + if ((unsigned) call_expr_nargs (exp) <= argno) return; - if (call_expr_nargs (exp) < 1) - return; - - tree ptr = CALL_EXPR_ARG (exp, 0); + tree ptr = CALL_EXPR_ARG (exp, argno); if (integer_zerop (ptr)) return; access_ref aref; - if (!compute_objsize (ptr, 0, &aref) || !aref.ref) + if (!compute_objsize (ptr, 0, &aref)) return; tree ref = aref.ref; - if (integer_zerop (ref)) return; @@ -13258,14 +13362,21 @@ maybe_emit_free_warning (tree exp) return; } else - warned = warning_at (loc, OPT_Wmismatched_new_delete, - "%K%qD called on pointer returned " - "from a mismatched allocation " - "function", exp, dealloc_decl); + { + tree alloc_decl = gimple_call_fndecl (def_stmt); + int opt = (DECL_IS_OPERATOR_NEW_P (alloc_decl) + || DECL_IS_OPERATOR_DELETE_P (dealloc_decl) + ? OPT_Wmismatched_new_delete + : OPT_Wmismatched_dealloc); + warned = warning_at (loc, opt, + "%K%qD called on pointer returned " + "from a mismatched allocation " + "function", exp, dealloc_decl); + } } else if (gimple_call_builtin_p (def_stmt, BUILT_IN_ALLOCA) - || gimple_call_builtin_p (def_stmt, - BUILT_IN_ALLOCA_WITH_ALIGN)) + || gimple_call_builtin_p (def_stmt, + BUILT_IN_ALLOCA_WITH_ALIGN)) warned = warning_at (loc, OPT_Wfree_nonheap_object, "%K%qD called on pointer to " "an unallocated object", diff --git a/gcc/c-family/c-attribs.c b/gcc/c-family/c-attribs.c index b979fbcc0c6..c0d198dca63 100644 --- a/gcc/c-family/c-attribs.c +++ b/gcc/c-family/c-attribs.c @@ -113,6 +113,7 @@ static tree handle_no_instrument_function_attribute (tree *, tree, static tree handle_no_profile_instrument_function_attribute (tree *, tree, tree, int, bool *); static tree handle_malloc_attribute (tree *, tree, tree, int, bool *); +static tree handle_dealloc_attribute (tree *, tree, tree, int, bool *); static tree handle_returns_twice_attribute (tree *, tree, tree, int, bool *); static tree handle_no_limit_stack_attribute (tree *, tree, tree, int, bool *); @@ -361,7 +362,7 @@ const struct attribute_spec c_common_attribute_table[] = { "no_profile_instrument_function", 0, 0, true, false, false, false, handle_no_profile_instrument_function_attribute, NULL }, - { "malloc", 0, 0, true, false, false, false, + { "malloc", 0, 2, true, false, false, false, handle_malloc_attribute, attr_alloc_exclusions }, { "returns_twice", 0, 0, true, false, false, false, handle_returns_twice_attribute, @@ -519,6 +520,8 @@ const struct attribute_spec c_common_attribute_table[] = handle_objc_root_class_attribute, NULL }, { "objc_nullability", 1, 1, true, false, false, false, handle_objc_nullability_attribute, NULL }, + { "*dealloc", 1, 2, true, false, false, false, + handle_dealloc_attribute, NULL }, { NULL, 0, 0, false, false, false, false, NULL, NULL } }; @@ -3072,20 +3075,179 @@ handle_no_profile_instrument_function_attribute (tree *node, tree name, tree, return NULL_TREE; } -/* Handle a "malloc" attribute; arguments as in - struct attribute_spec.handler. */ +/* Handle the "malloc" attribute. */ static tree -handle_malloc_attribute (tree *node, tree name, tree ARG_UNUSED (args), +handle_malloc_attribute (tree *node, tree name, tree args, int ARG_UNUSED (flags), bool *no_add_attrs) { - if (TREE_CODE (*node) == FUNCTION_DECL - && POINTER_TYPE_P (TREE_TYPE (TREE_TYPE (*node)))) - DECL_IS_MALLOC (*node) = 1; - else + tree fndecl = *node; + + if (TREE_CODE (*node) != FUNCTION_DECL) { - warning (OPT_Wattributes, "%qE attribute ignored", name); + warning (OPT_Wattributes, "%qE attribute ignored; valid only " + "for functions", + name); *no_add_attrs = true; + return NULL_TREE; + } + + tree rettype = TREE_TYPE (TREE_TYPE (*node)); + if (!POINTER_TYPE_P (rettype)) + { + warning (OPT_Wattributes, "%qE attribute ignored on functions " + "returning %qT; valid only for pointer return types", + name, rettype); + *no_add_attrs = true; + return NULL_TREE; + } + + if (!args) + { + /* Only the form of the attribute with no arguments declares + a function malloc-like. */ + DECL_IS_MALLOC (*node) = 1; + return NULL_TREE; + } + + tree dealloc = TREE_VALUE (args); + if (error_operand_p (dealloc)) + { + /* If the argument is in error it will have already been diagnosed. + Avoid issuing redundant errors here. */ + *no_add_attrs = true; + return NULL_TREE; + } + + /* In C++ the argument may be wrapped in a cast to disambiguate one + of a number of overloads (such as operator delete). Strip it. */ + STRIP_NOPS (dealloc); + if (TREE_CODE (dealloc) == ADDR_EXPR) + dealloc = TREE_OPERAND (dealloc, 0); + + if (TREE_CODE (dealloc) != FUNCTION_DECL) + { + if (TREE_CODE (dealloc) == OVERLOAD) + { + /* Handle specially the common case of specifying one of a number + of overloads, such as operator delete. */ + error ("%qE attribute argument 1 is ambiguous", name); + inform (input_location, + "use a cast to the expected type to disambiguate"); + *no_add_attrs = true; + return NULL_TREE; + } + + error ("%qE attribute argument 1 does not name a function", name); + if (DECL_P (dealloc)) + inform (DECL_SOURCE_LOCATION (dealloc), + "argument references a symbol declared here"); + *no_add_attrs = true; + return NULL_TREE; + } + + /* Mentioning the deallocation function qualifies as its use. */ + TREE_USED (dealloc) = 1; + + tree fntype = TREE_TYPE (dealloc); + tree argpos = TREE_CHAIN (args) ? TREE_VALUE (TREE_CHAIN (args)) : NULL_TREE; + if (!argpos) + { + tree argtypes = TYPE_ARG_TYPES (fntype); + if (!argtypes) + { + /* Reject functions without a prototype. */ + error ("%qE attribute argument 1 must take a pointer " + "type as its first argument", name); + inform (DECL_SOURCE_LOCATION (dealloc), + "refernced symbol declared here" ); + *no_add_attrs = true; + return NULL_TREE; + } + + tree argtype = TREE_VALUE (argtypes); + if (TREE_CODE (argtype) != POINTER_TYPE) + { + /* Reject functions that don't take a pointer as their first + argument. */ + error ("%qE attribute argument 1 must take a pointer type " + "as its first argument; have %qT", name, argtype); + inform (DECL_SOURCE_LOCATION (dealloc), + "referenced symbol declared here" ); + *no_add_attrs = true; + return NULL_TREE; + } + + *no_add_attrs = false; + tree attr_free = build_tree_list (NULL_TREE, DECL_NAME (fndecl)); + attr_free = build_tree_list (get_identifier ("*dealloc"), attr_free); + decl_attributes (&dealloc, attr_free, 0); + return NULL_TREE; + } + + /* Validate the positional argument. */ + argpos = positional_argument (fntype, name, argpos, POINTER_TYPE); + if (!argpos) + { + *no_add_attrs = true; + return NULL_TREE; + } + + /* It's valid to declare the same function with multiple instances + of attribute malloc, each naming the same or different deallocator + functions, and each referencing either the same or a different + positional argument. */ + *no_add_attrs = false; + tree attr_free = tree_cons (NULL_TREE, argpos, NULL_TREE); + attr_free = tree_cons (NULL_TREE, DECL_NAME (fndecl), attr_free); + attr_free = build_tree_list (get_identifier ("*dealloc"), attr_free); + decl_attributes (&dealloc, attr_free, 0); + return NULL_TREE; +} + +/* Handle the internal "*dealloc" attribute added for functions declared + with the one- and two-argument forms of attribute malloc. Add it + to *NODE unless it's already there with the same arguments. */ + +static tree +handle_dealloc_attribute (tree *node, tree name, tree args, int, + bool *no_add_attrs) +{ + tree fndecl = *node; + + tree attrs = DECL_ATTRIBUTES (fndecl); + if (!attrs) + return NULL_TREE; + + tree arg_fname = TREE_VALUE (args); + args = TREE_CHAIN (args); + tree arg_pos = args ? TREE_VALUE (args) : NULL_TREE; + + gcc_checking_assert (TREE_CODE (arg_fname) == IDENTIFIER_NODE); + + const char* const namestr = IDENTIFIER_POINTER (name); + for (tree at = attrs; (at = lookup_attribute (namestr, at)); + at = TREE_CHAIN (at)) + { + tree alloc = TREE_VALUE (at); + if (!alloc) + continue; + + tree pos = TREE_CHAIN (alloc); + alloc = TREE_VALUE (alloc); + pos = pos ? TREE_VALUE (pos) : NULL_TREE; + gcc_checking_assert (TREE_CODE (alloc) == IDENTIFIER_NODE); + + if (alloc == arg_fname + && ((!pos && !arg_pos) + || (pos && arg_pos && tree_int_cst_equal (pos, arg_pos)))) + { + /* The function already has the attribute either without any + arguments or with the same arguments as the attribute that's + being added. Return without adding another copy. */ + *no_add_attrs = true; + return NULL_TREE; + } } return NULL_TREE; diff --git a/gcc/c-family/c.opt b/gcc/c-family/c.opt index 0bc81e696db..a1b57043d38 100644 --- a/gcc/c-family/c.opt +++ b/gcc/c-family/c.opt @@ -785,10 +785,15 @@ Wmisleading-indentation C C++ Common Var(warn_misleading_indentation) Warning LangEnabledBy(C C++,Wall) Warn when the indentation of the code does not reflect the block structure. +Wmismatched-dealloc +C ObjC C++ ObjC++ Var(warn_mismatched_alloc) Warning LangEnabledBy(C ObjC C++ ObjC++,Wall) +Warn for deallocation calls with arguments returned from mismatched allocation +functions. + Wmismatched-new-delete C++ ObjC++ Var(warn_mismatched_new_delete) Warning LangEnabledBy(C ObjC C++ ObjC++,Wall) -Warn for deallocation calls with arguments returned from calls to mismatched -allocation functions. +Warn for mismatches between calls to operator new or delete and the corrsponding +call to the allocation or deallocation function. Wmismatched-tags C++ ObjC++ Var(warn_mismatched_tags) Warning diff --git a/gcc/doc/extend.texi b/gcc/doc/extend.texi index af25f66c8b4..2ed90f0e613 100644 --- a/gcc/doc/extend.texi +++ b/gcc/doc/extend.texi @@ -3233,20 +3233,63 @@ this reason the attribute is not allowed on types to annotate indirect calls. @item malloc +@item malloc (@var{deallocator}) +@item malloc (@var{deallocator}, @var{ptr-index}) @cindex @code{malloc} function attribute @cindex functions that behave like malloc -This tells the compiler that a function is @code{malloc}-like, i.e., -that the pointer @var{P} returned by the function cannot alias any +Attribute @code{malloc} indicates that a function is @code{malloc}-like, +i.e., that the pointer @var{P} returned by the function cannot alias any other pointer valid when the function returns, and moreover no pointers to valid objects occur in any storage addressed by @var{P}. -Using this attribute can improve optimization. Compiler predicts -that a function with the attribute returns non-null in most cases. -Functions like -@code{malloc} and @code{calloc} have this property because they return -a pointer to uninitialized or zeroed-out storage. However, functions -like @code{realloc} do not have this property, as they can return a -pointer to storage containing pointers. +Independently, the form of the attribute with one or two arguments +associates @code{deallocator} as a suitable deallocation function for +pointers returned from the @code{malloc}-like function. @var{ptr-index} +denotes the positional argument to which when the pointer is passed in +calls to @code{deallocator} has the effect of deallocating it. + +Using the attribute with no arguments is designed to improve optimization. +The compiler predicts that a function with the attribute returns non-null +in most cases. Functions like @code{malloc} and @code{calloc} have this +property because they return a pointer to uninitialized or zeroed-out +storage. However, functions like @code{realloc} do not have this property, +as they may return pointers to storage containing pointers to existing +objects. + +Associating a function with a @var{deallocator} helps detect calls to +mismatched allocation and deallocation functions and diagnose them +under the control of options such as @option{-Wmismatched-dealloc}. +To indicate that an allocation function both satisifies the nonaliasing +property and has a deallocator associated with it, both the plain form +of the attribute and the one with the @var{deallocator} argument must +be used. + +For example, besides stating that the functions return pointers that do +not alias any others, the following declarations make the @code{fclose} +and @code{frepen} functions suitable deallocators for pointers returned +from all the functions that return them, and the @code{pclose} function +as the only other suitable deallocator besides @code{freopen} for pointers +returned from @code{popen}. The deallocator functions must declared +before they can be referenced in the attribute. + +@smallexample +int fclose (FILE*); +FILE* freopen (const char*, const char*, FILE*); +int pclose (FILE*); + +__attribute__ ((malloc, malloc (fclose), malloc (freopen, 3))) + FILE* fdopen (int); +__attribute__ ((malloc, malloc (fclose), malloc (freopen, 3))) + FILE* fopen (const char*, const char*); +__attribute__ ((malloc, malloc (fclose), malloc (freopen, 3))) + FILE* fmemopen(void *, size_t, const char *); +__attribute__ ((malloc, malloc (fclose), malloc (freopen, 3))) + FILE* freopen (const char*, const char*, FILE*); +__attribute__ ((malloc, malloc (pclose), malloc (freopen, 3))) + FILE* popen (const char*, const char*); +__attribute__ ((malloc, malloc (fclose), malloc (freopen, 3))) + FILE* tmpfile (void); +@end smallexample @item no_icf @cindex @code{no_icf} function attribute diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi index 2e997b03447..e32f2f8df9d 100644 --- a/gcc/doc/invoke.texi +++ b/gcc/doc/invoke.texi @@ -3834,13 +3834,19 @@ templates. @item -Wno-mismatched-new-delete @r{(C++ and Objective-C++ only)} @opindex Wmismatched-new-delete @opindex Wno-mismatched-new-delete -Warn for invocations of C++ @code{operator delete} with pointer arguments +Warn for mismatches between calls to @code{operator new} or @code{operator +delete} and the corresponding call to the allocation or deallocation function. +This includes invocations of C++ @code{operator delete} with pointers returned from either mismatched forms of @code{operator new}, or from other functions that allocate objects for which the @code{operator delete} isn't -a suitable deallocator. For example, the @code{delete} expression in -the function below is diagnosed because it doesn't match the array form -of the @code{new} expression the pointer argument was returned from. -Similarly, the call to @code{free} is also diagnosed. +a suitable deallocator, as well as calls to other deallocation functions +with pointers returned from @code{operator new} for which the deallocation +function isn't suitable. + +For example, the @code{delete} expression in the function below is diagnosed +because it doesn't match the array form of the @code{new} expression +the pointer argument was returned from. Similarly, the call to @code{free} +is also diagnosed. @smallexample void f () @@ -3853,6 +3859,10 @@ void f () @} @end smallexample +The related option @option{-Wmismatched-dealloc} diagnoses mismatches +involving allocation and deallocation functions other than @code{operator +new} and @code{operator delete}. + @option{-Wmismatched-new-delete} is enabled by default. @item -Wmismatched-tags @r{(C++ and Objective-C++ only)} @@ -6283,6 +6293,41 @@ Ignoring the warning can result in poorly optimized code. disable the warning, but this is not recommended and should be done only when non-existent profile data is justified. +@item -Wno-mismatched-dealloc +@opindex Wmismatched-dealloc +@opindex Wno-mismatched-dealloc + +Warn for calls to deallocation functions with pointer arguments returned +from from allocations functions for which the former isn't a suitable +deallocator. A pair of functions can be associated as matching allocators +and deallocators by use of attribute @code{malloc}. Unless disabled by +the @option{-fno-builtin} option the standard functions @code{calloc}, +@code{malloc}, @code{realloc}, and @code{free}, as well as the corresponding +forms of C++ @code{operator new} and @code{operator delete} are implicitly +associated as matching allocators and deallocators. In the following +example @code{mydealloc} is the deallocator for pointers returned from +@code{myalloc}. + +@smallexample +void mydealloc (void*); + +__attribute__ ((malloc (mydealloc, 1))) void* +myalloc (size_t); + +void f (void) +@{ + void *p = myalloc (32); + // @dots{}use p@dots{} + free (p); // warning: not a matching deallocator for myalloc + mydealloc (p); // ok +@} +@end smallexample + +In C++, the related option @option{-Wmismatched-new-delete} diagnoses +mismatches involving either @code{operator new} or @code{operator delete}. + +Option @option{-Wmismatched-dealloc} is enabled by default. + @item -Wmultistatement-macros @opindex Wmultistatement-macros @opindex Wno-multistatement-macros diff --git a/gcc/testsuite/g++.dg/warn/Wmismatched-dealloc-2.C b/gcc/testsuite/g++.dg/warn/Wmismatched-dealloc-2.C new file mode 100644 index 00000000000..7ecc99a325c --- /dev/null +++ b/gcc/testsuite/g++.dg/warn/Wmismatched-dealloc-2.C @@ -0,0 +1,185 @@ +/* PR middle-end/94527 - Add an attribute that marks a function as freeing + an object + The detection doesn't require optimization. + { dg-do compile } + { dg-options "-Wall" } */ + +#define A(...) __attribute__ ((malloc (__VA_ARGS__))) + +typedef __SIZE_TYPE__ size_t; + +extern "C" { + void free (void *); + void* realloc (void *, size_t); +} + +void sink (void *); + +void mydealloc (int, void*); +void* A (mydealloc, 2) myalloc (void*); + + +void my_delete (const char*, void*); +void my_array_delete (const char*, void*); + +typedef void OpDelete1 (void*); +typedef void OpDelete2 (void*, size_t); + +A ((OpDelete1*)operator delete, 1) +#if __cplusplus >= 201402L +A ((OpDelete2*)operator delete, 1) +#endif +A (my_delete, 2) +int* my_new (size_t); + +A ((OpDelete1*)operator delete[], 1) +#if __cplusplus >= 201402L +A ((OpDelete2*)operator delete[], 1) +#endif +A (my_array_delete, 2) +int* my_array_new (size_t); + + +void test_my_new () +{ + { + void *p = my_new (1); + operator delete (p); + } + { + void *p = my_new (1); + sink (p); + operator delete (p); + } + { + int *p = my_new (1); + sink (p); + delete p; + } + + { + void *p = my_new (1); + // { dg-message "returned from a call to 'int\\\* my_new\\\(size_t\\\)'" "note" { target *-*-* } .-1 } + operator delete[] (p); + // { dg-warning "'void operator delete \\\[]\\\(void\\\*\\\)' called on pointer returned from a mismatched allocation function \\\[-Wmismatched-new-delete" "" { target *-*-* } .-1 } + } + { + void *p = my_new (1); + // { dg-message "returned from a call to 'int\\\* my_new\\\(size_t\\\)'" "note" { target *-*-* } .-1 } + sink (p); + operator delete[] (p); + // { dg-warning "'void operator delete \\\[]\\\(void\\\*\\\)' called on pointer returned from a mismatched allocation function \\\[-Wmismatched-new-delete" "" { target *-*-* } .-1 } + } + { + int *p = my_new (1); + sink (p); + delete[] p; + // { dg-warning "'void operator delete \\\[]\\\(void\\\*\\\)' called on pointer returned from a mismatched allocation function \\\[-Wmismatched-new-delete" "" { target *-*-* } .-1 } + } + + { + void *p = my_new (1); + my_delete ("1", p); + } + { + void *p = my_new (1); + sink (p); + my_delete ("2", p); + } + + { + void *p = my_new (1); + // { dg-message "returned from a call to 'int\\\* my_new\\\(size_t\\\)'" "note" { target *-*-* } .-1 } + sink (p); + my_array_delete ("3", p); + // { dg-warning "'void my_array_delete\\\(const char\\\*, void\\\*\\\)' called on pointer returned from a mismatched allocation function" "" { target *-*-* } .-1 } + } + + { + void *p = my_new (1); + // { dg-message "returned from a call to 'int\\\* my_new\\\(size_t\\\)'" "note" { target *-*-* } .-1 } + sink (p); + free (p); + // { dg-warning "'void free\\\(void\\\*\\\)' called on pointer returned from a mismatched allocation function" "" { target *-*-* } .-1 } + } + + { + void *p = my_new (1); + // { dg-message "returned from a call to 'int\\\* my_new\\\(size_t\\\)'" "note" { target *-*-* } .-1 } + sink (p); + p = realloc (p, 123); + // { dg-warning "'void\\\* realloc\\\(void\\\*, size_t\\\)' called on pointer returned from a mismatched allocation function" "" { target *-*-* } .-1 } + } +} + + +void test_my_array_new () +{ + { + void *p = my_array_new (1); + operator delete[] (p); + } + { + void *p = my_array_new (1); + sink (p); + operator delete[] (p); + } + { + int *p = my_array_new (1); + sink (p); + delete[] p; + } + + { + void *p = my_array_new (1); + // { dg-message "returned from a call to 'int\\\* my_array_new\\\(size_t\\\)'" "note" { target *-*-* } .-1 } + operator delete (p); + // { dg-warning "'void operator delete\\\(void\\\*\\\)' called on pointer returned from a mismatched allocation function \\\[-Wmismatched-new-delete" "" { target *-*-* } .-1 } + } + { + void *p = my_array_new (1); + // { dg-message "returned from a call to 'int\\\* my_array_new\\\(size_t\\\)'" "note" { target *-*-* } .-1 } + sink (p); + operator delete (p); + // { dg-warning "'void operator delete\\\(void\\\*\\\)' called on pointer returned from a mismatched allocation function \\\[-Wmismatched-new-delete" "" { target *-*-* } .-1 } + } + { + int *p = my_array_new (1); + sink (p); + delete p; + // { dg-warning "'void operator delete\\\(void\\\*\[^\)\]*\\\)' called on pointer returned from a mismatched allocation function \\\[-Wmismatched-new-delete" "" { target *-*-* } .-1 } + } + + { + void *p = my_array_new (1); + my_array_delete ("1", p); + } + { + void *p = my_array_new (1); + sink (p); + my_array_delete ("2", p); + } + { + void *p = my_array_new (1); + // { dg-message "returned from a call to 'int\\\* my_array_new\\\(size_t\\\)'" "note" { target *-*-* } .-1 } + sink (p); + my_delete ("3", p); + // { dg-warning "'void my_delete\\\(const char\\\*, void\\\*\\\)' called on pointer returned from a mismatched allocation function" "" { target *-*-* } .-1 } + } + + { + void *p = my_array_new (1); + // { dg-message "returned from a call to 'int\\\* my_array_new\\\(size_t\\\)'" "note" { target *-*-* } .-1 } + sink (p); + free (p); + // { dg-warning "'void free\\\(void\\\*\\\)' called on pointer returned from a mismatched allocation function" "" { target *-*-* } .-1 } + } + + { + void *p = my_array_new (1); + // { dg-message "returned from a call to 'int\\\* my_array_new\\\(size_t\\\)'" "note" { target *-*-* } .-1 } + sink (p); + p = realloc (p, 123); + // { dg-warning "'void\\\* realloc\\\(void\\\*, size_t\\\)' called on pointer returned from a mismatched allocation function" "" { target *-*-* } .-1 } + } +} diff --git a/gcc/testsuite/g++.dg/warn/Wmismatched-dealloc.C b/gcc/testsuite/g++.dg/warn/Wmismatched-dealloc.C new file mode 100644 index 00000000000..682db6f02cb --- /dev/null +++ b/gcc/testsuite/g++.dg/warn/Wmismatched-dealloc.C @@ -0,0 +1,27 @@ +/* PR middle-end/94527 - Add an attribute that marks a function as freeing + an object + { dg-do compile { target c++11 } } + { dg-options "-Wall" } */ + +#define A(...) __attribute__ ((malloc (__VA_ARGS__))) + +typedef __SIZE_TYPE__ size_t; + +void mydealloc (int, void*); +void* A (mydealloc, 2) myalloc (void*); + + +void* A (operator delete, 1) + bad_new (size_t); // { dg-error "attribute argument 1 is ambiguous" } +void* A (operator delete[], 1) + bad_array_new (size_t); // { dg-error "attribute argument 1 is ambiguous" } + +void my_delete (const char*, void*); +void my_array_delete (const char*, void*); + +typedef void OpDelete (void*); + +int* A ((OpDelete*)operator delete, 1) A (my_delete, 2) + my_new (size_t); +int* A ((OpDelete*)operator delete[], 1) A (my_array_delete, 2) + my_array_new (size_t); diff --git a/gcc/testsuite/gcc.dg/Wmismatched-dealloc.c b/gcc/testsuite/gcc.dg/Wmismatched-dealloc.c new file mode 100644 index 00000000000..7c5d6acf4d6 --- /dev/null +++ b/gcc/testsuite/gcc.dg/Wmismatched-dealloc.c @@ -0,0 +1,252 @@ +/* PR middle-end/94527 - Add an attribute that marks a function as freeing + an object + Verify that attribute malloc with one or two arguments has the expected + effect on diagnostics. + { dg-options "-Wall -ftrack-macro-expansion=0" } */ + +#define A(...) __attribute__ ((malloc (__VA_ARGS__))) + +typedef struct FILE FILE; +typedef __SIZE_TYPE__ size_t; + +void free (void*); +void* malloc (size_t); +void* realloc (void*, size_t); + +int fclose (FILE*); +FILE* freopen (const char*, const char*, FILE*); +int pclose (FILE*); + +A (fclose) A (freopen, 3) + FILE* fdopen (int); +A (fclose) A (freopen, 3) + FILE* fopen (const char*, const char*); +A (fclose) A (freopen, 3) + FILE* fmemopen(void *, size_t, const char *); +A (fclose) A (freopen, 3) + FILE* freopen (const char*, const char*, FILE*); +A (pclose) A (freopen, 3) + FILE* popen (const char*, const char*); +A (fclose) A (freopen, 3) + FILE* tmpfile (void); + +void sink (FILE*); + + + void release (void*); +A (release) FILE* acquire (void); + +void nowarn_fdopen (void) +{ + { + FILE *q = fdopen (0); + if (!q) + return; + + fclose (q); + } + + { + FILE *q = fdopen (0); + if (!q) + return; + + q = freopen ("1", "r", q); + fclose (q); + } + + { + FILE *q = fdopen (0); + if (!q) + return; + + sink (q); + } +} + + +void warn_fdopen (void) +{ + { + FILE *q = fdopen (0); // { dg-message "returned from a call to 'fdopen'" "note" } + sink (q); + release (q); // { dg-warning "'release' called on pointer returned from a mismatched allocation function" } + } + { + FILE *q = fdopen (0); // { dg-message "returned from a call to 'fdopen'" "note" } + sink (q); + free (q); // { dg-warning "'free' called on pointer returned from a mismatched allocation function" } + } + + { + FILE *q = fdopen (0); // { dg-message "returned from a call to 'fdopen'" "note" } + sink (q); + q = realloc (q, 7); // { dg-warning "'realloc' called on pointer returned from a mismatched allocation function" } + sink (q); + } +} + + +void nowarn_fopen (void) +{ + { + FILE *q = fopen ("1", "r"); + sink (q); + fclose (q); + } + + { + FILE *q = fopen ("2", "r"); + sink (q); + q = freopen ("3", "r", q); + sink (q); + fclose (q); + } + + { + FILE *q = fopen ("4", "r"); + sink (q); + } +} + + +void warn_fopen (void) +{ + { + FILE *q = fopen ("1", "r"); + sink (q); + release (q); // { dg-warning "'release' called on pointer returned from a mismatched allocation function" } + } + { + FILE *q = fdopen (0); + sink (q); + free (q); // { dg-warning "'free' called on pointer returned from a mismatched allocation function" } + } + + { + FILE *q = fdopen (0); + sink (q); + q = realloc (q, 7); // { dg-warning "'realloc' called on pointer returned from a mismatched allocation function" } + sink (q); + } +} + + +void test_popen (void) +{ + { + FILE *p = popen ("1", "r"); + sink (p); + pclose (p); + } + + { + FILE *p; + p = popen ("2", "r"); // { dg-message "returned from a call to 'popen'" "note" } + sink (p); + fclose (p); // { dg-warning "'fclose' called on pointer returned from a mismatched allocation function" } + } + + { + /* freopen() can close a stream open by popen() but pclose() can't + close the stream returned from freopen(). */ + FILE *p = popen ("2", "r"); + sink (p); + p = freopen ("3", "r", p); // { dg-message "returned from a call to 'freopen'" "note" } + sink (p); + pclose (p); // { dg-warning "'pclose' called on pointer returned from a mismatched allocation function" } + } +} + + +void test_tmpfile (void) +{ + { + FILE *p = tmpfile (); + sink (p); + fclose (p); + } + + { + FILE *p = tmpfile (); + sink (p); + p = freopen ("1", "r", p); + sink (p); + fclose (p); + } + + { + FILE *p = tmpfile (); // { dg-message "returned from a call to 'tmpfile'" "note" } + sink (p); + pclose (p); // { dg-warning "'pclose' called on pointer returned from a mismatched allocation function" } + } +} + + +void warn_malloc (void) +{ + { + FILE *p = malloc (100); // { dg-message "returned from a call to 'malloc'" "note" } + sink (p); + fclose (p); // { dg-warning "'fclose' called on pointer returned from a mismatched allocation function" } + } + + { + FILE *p = malloc (100); // { dg-message "returned from a call to 'malloc'" "note" } + sink (p); + p = freopen ("1", "r", p);// { dg-warning "'freopen' called on pointer returned from a mismatched allocation function" } + } + + { + FILE *p = malloc (100); // { dg-message "returned from a call to 'malloc'" "note" } + sink (p); + pclose (p); // { dg-warning "'pclose' called on pointer returned from a mismatched allocation function" } + } +} + + +void test_acquire (void) +{ + { + FILE *p = acquire (); + release (p); + } + + { + FILE *p = acquire (); + sink (p); + release (p); + } + + { + FILE *p = acquire (); // { dg-message "returned from a call to 'acquire'" "note" } + sink (p); + fclose (p); // { dg-warning "'fclose' called on pointer returned from a mismatched allocation function" } + } + + { + FILE *p = acquire (); // { dg-message "returned from a call to 'acquire'" "note" } + sink (p); + pclose (p); // { dg-warning "'pclose' called on pointer returned from a mismatched allocation function" } + } + + { + FILE *p = acquire (); // { dg-message "returned from a call to 'acquire'" "note" } + sink (p); + p = freopen ("1", "r", p); // { dg-warning "'freopen' called on pointer returned from a mismatched allocation function" } + sink (p); + } + + { + FILE *p = acquire (); // { dg-message "returned from a call to 'acquire'" "note" } + sink (p); + free (p); // { dg-warning "'free' called on pointer returned from a mismatched allocation function" } + } + + { + FILE *p = acquire (); // { dg-message "returned from a call to 'acquire'" "note" } + sink (p); + p = realloc (p, 123); // { dg-warning "'realloc' called on pointer returned from a mismatched allocation function" } + sink (p); + } +} diff --git a/gcc/testsuite/gcc.dg/attr-malloc.c b/gcc/testsuite/gcc.dg/attr-malloc.c new file mode 100644 index 00000000000..14f1980ed7f --- /dev/null +++ b/gcc/testsuite/gcc.dg/attr-malloc.c @@ -0,0 +1,75 @@ +/* PR middle-end/94527 - Add an attribute that marks a function as freeing + an object + Verify that attribute malloc with one or two arguments is accepted where + intended and rejected where it's invalid. + { dg-options "-Wall -ftrack-macro-expansion=0" } */ + +#define A(...) __attribute__ ((malloc (__VA_ARGS__))) + +A (0) void* alloc_zero (int); // { dg-error "'malloc' attribute argument 1 does not name a function" } + +A ("") void* alloc_string (int); // { dg-error "'malloc' attribute argument 1 does not name a function" } + +int var; +A (var) void* alloc_var (int); // { dg-error "'malloc' attribute argument 1 does not name a function" } + +typedef struct Type { int i; } Type; +A (Type) void* alloc_type (int); // { dg-error "expected expression|identifier" } + +A (unknown) void* alloc_unknown (int); // { dg-error "'unknown' undeclared" } + +void fv_ (); // { dg-message "declared here" } +A (fv_) void* alloc_fv_ (int); // { dg-error "'malloc' attribute argument 1 must take a pointer type as its first argument" } + +void fvi (int); // { dg-message "declared here" } +A (fvi) void* alloc_fvi (int); // { dg-error "'malloc' attribute argument 1 must take a pointer type as its first argument; have 'int'" } + +void fvv (void); // { dg-message "declared here" } +A (fvv) void* alloc_fvv (int); // { dg-error "'malloc' attribute argument 1 must take a pointer type as its first argument; have 'void'" } + +void fvi_ (int, ...); // { dg-message "declared here" } +A (fvi_) void* alloc_fvi_ (int); // { dg-error "'malloc' attribute argument 1 must take a pointer type as its first argument; have 'int'" } + +void fvi_vp (Type, void*); // { dg-message "declared here" } +A (fvi_vp) void* alloc_fvi_vp (int); // { dg-error "'malloc' attribute argument 1 must take a pointer type as its first argument; have 'Type'" } + + +void fpv (void*); +A (fpv) void* alloc_fpv (int); + +void fpv_i (void*, int); +A (fpv_i) void* alloc_fpv_i (int); + +void fpv_pv (void*, void*); +A (fpv_i) void* alloc_fpv_pv (int); + + +void gpc (char*); +void hpi (int*); +A (fpv) A (gpc) A (hpi) Type* alloc_fpv_gpv (int); + + +/* Verify that the attribute can be applied to <stdio.h> functions. */ +typedef struct FILE FILE; +typedef __SIZE_TYPE__ size_t; + +int fclose (FILE*); +FILE* fdopen (int); +FILE* fopen (const char*, const char*); +FILE* freopen (const char*, const char*, FILE*); +int pclose (FILE*); +FILE* popen (const char*, const char*); +FILE* tmpfile (void); + +A (fclose) A (freopen, 3) A (pclose) + FILE* fdopen (int); +A (fclose) A (freopen, 3) A (pclose) + FILE* fopen (const char*, const char*); +A (fclose) A (freopen, 3) A (pclose) + FILE* fmemopen(void *, size_t, const char *); +A (fclose) A (freopen, 3) A (pclose) + FILE* freopen (const char*, const char*, FILE*); +A (fclose) A (freopen, 3) A (pclose) + FILE* popen (const char*, const char*); +A (fclose) A (freopen, 3) A (pclose) + FILE* tmpfile (void);