diff options
author | benjamin priour <priour.be@gmail.com> | 2023-09-01 00:01:29 +0200 |
---|---|---|
committer | benjamin priour <vultkayn@gcc.gnu.org> | 2023-09-01 22:03:34 +0200 |
commit | e7b267444045c507654a2a3f758efee5d5b550df (patch) | |
tree | 1aa37e6cab63d98b2948bd00344fc93badc113b7 /gcc/analyzer | |
parent | d3dd69706af4c086cb3385ff1f321887b91f49fb (diff) | |
download | gcc-e7b267444045c507654a2a3f758efee5d5b550df.zip gcc-e7b267444045c507654a2a3f758efee5d5b550df.tar.gz gcc-e7b267444045c507654a2a3f758efee5d5b550df.tar.bz2 |
analyzer: Add support of placement new and improved operator new [PR105948,PR94355]
Fixed spurious possibly-NULL warning always tagging along throwing
operator new despite it never returning NULL.
Now operator new is correctly recognized as possibly returning NULL
if and only if it is non-throwing or exceptions have been disabled.
Different standard signatures of operator new are now properly
recognized.
Added support of placement new, so that it is now properly recognized,
and a 'heap_allocated' region is no longer created for it.
Placement new size is also checked and a 'Wanalyzer-allocation-size'
is emitted when relevant, as well as always a 'Wanalyzer-out-of-bounds'.
'operator new' non-throwing variants are detected y checking the types
of the parameters.
Indeed, in a call to new (std::nothrow) () the chosen overload
has signature 'operator new (void*, std::nothrow_t&)', where the second
parameter is a reference. In a placement new, the second parameter will
always be a void pointer.
Prior to this patch, some buffers first allocated with 'new', then deleted
an thereafter used would result in a 'Wanalyzer-user-after-free'
warning. However the wording was "use after 'free'" instead of the
expected "use after 'delete'".
This patch fixes this by introducing a new kind of poisoned value,
namely POISON_KIND_DELETED.
Due to how the analyzer sees calls to non-throwing variants of
operator new, dereferencing a pointer freshly allocated in this fashion
caused both a 'Wanalyzer-use-of-uninitialized-value' and a
'Wanalyzer-null-dereference' to be emitted, while only the latter was
relevant. As a result, 'null-dereference' now supersedes
'use-of-uninitialized'.
Signed-off-by: benjamin priour <vultkayn@gcc.gnu.org>
gcc/analyzer/ChangeLog:
PR analyzer/105948
PR analyzer/94355
* analyzer.h (is_placement_new_p): New declaration.
* call-details.cc
(call_details::deref_ptr_arg): New function.
Dereference the argument at given index if possible.
* call-details.h: Declaration of the above function.
* kf-lang-cp.cc (is_placement_new_p): Returns true if the gcall
is recognized as a placement new.
(kf_operator_delete::impl_call_post): Unbinding a region and its
descendents now poisons with POISON_KIND_DELETED.
(register_known_functions_lang_cp): Known function "operator
delete" is now registered only once independently of its number of
arguments.
* region-model.cc (region_model::eval_condition): Now
recursively calls itself if any of the operand is wrapped in a
cast.
* sm-malloc.cc (malloc_state_machine::on_stmt):
Add placement new recognition.
* svalue.cc (poison_kind_to_str): Wording for the new PK.
* svalue.h (enum poison_kind): Add value POISON_KIND_DELETED.
gcc/testsuite/ChangeLog:
PR analyzer/105948
PR analyzer/94355
* g++.dg/analyzer/out-of-bounds-placement-new.C: Added a directive.
* g++.dg/analyzer/placement-new.C: Added tests.
* g++.dg/analyzer/new-2.C: New test.
* g++.dg/analyzer/noexcept-new.C: New test.
* g++.dg/analyzer/placement-new-size.C: New test.
Diffstat (limited to 'gcc/analyzer')
-rw-r--r-- | gcc/analyzer/analyzer.h | 1 | ||||
-rw-r--r-- | gcc/analyzer/call-details.cc | 11 | ||||
-rw-r--r-- | gcc/analyzer/call-details.h | 1 | ||||
-rw-r--r-- | gcc/analyzer/kf-lang-cp.cc | 116 | ||||
-rw-r--r-- | gcc/analyzer/region-model.cc | 36 | ||||
-rw-r--r-- | gcc/analyzer/sm-malloc.cc | 37 | ||||
-rw-r--r-- | gcc/analyzer/svalue.cc | 2 | ||||
-rw-r--r-- | gcc/analyzer/svalue.h | 3 |
8 files changed, 182 insertions, 25 deletions
diff --git a/gcc/analyzer/analyzer.h b/gcc/analyzer/analyzer.h index 9b351b5..208b850 100644 --- a/gcc/analyzer/analyzer.h +++ b/gcc/analyzer/analyzer.h @@ -423,6 +423,7 @@ extern bool is_std_named_call_p (const_tree fndecl, const char *funcname, const gcall *call, unsigned int num_args); extern bool is_setjmp_call_p (const gcall *call); extern bool is_longjmp_call_p (const gcall *call); +extern bool is_placement_new_p (const gcall *call); extern const char *get_user_facing_name (const gcall *call); diff --git a/gcc/analyzer/call-details.cc b/gcc/analyzer/call-details.cc index 66fb0fe..9480f03 100644 --- a/gcc/analyzer/call-details.cc +++ b/gcc/analyzer/call-details.cc @@ -295,6 +295,17 @@ call_details::get_arg_svalue (unsigned idx) const return m_model->get_rvalue (arg, m_ctxt); } +/* If argument IDX's svalue at the callsite is of pointer type, + return the region it points to. + Otherwise return NULL. */ + +const region * +call_details::deref_ptr_arg (unsigned idx) const +{ + const svalue *ptr_sval = get_arg_svalue (idx); + return m_model->deref_rvalue (ptr_sval, get_arg_tree (idx), m_ctxt); +} + /* Attempt to get the string literal for argument IDX, or return NULL otherwise. For use when implementing "__analyzer_*" functions that take diff --git a/gcc/analyzer/call-details.h b/gcc/analyzer/call-details.h index 57b9d6e..976517b 100644 --- a/gcc/analyzer/call-details.h +++ b/gcc/analyzer/call-details.h @@ -61,6 +61,7 @@ public: tree get_arg_tree (unsigned idx) const; tree get_arg_type (unsigned idx) const; const svalue *get_arg_svalue (unsigned idx) const; + const region *deref_ptr_arg (unsigned idx) const; const char *get_arg_string_literal (unsigned idx) const; tree get_fndecl_for_call () const; diff --git a/gcc/analyzer/kf-lang-cp.cc b/gcc/analyzer/kf-lang-cp.cc index 393b4f2..c7ee622 100644 --- a/gcc/analyzer/kf-lang-cp.cc +++ b/gcc/analyzer/kf-lang-cp.cc @@ -35,6 +35,38 @@ along with GCC; see the file COPYING3. If not see #if ENABLE_ANALYZER +/* Return true if CALL is a non-allocating operator new or operator new [] + that contains no user-defined args, i.e. having any signature of: + + - void* operator new (std::size_t count, void* ptr); + - void* operator new[] (std::size_t count, void* ptr); + + See https://en.cppreference.com/w/cpp/memory/new/operator_new. */ + +bool is_placement_new_p (const gcall *call) +{ + gcc_assert (call); + tree fndecl = gimple_call_fndecl (call); + + if (!fndecl || TREE_CODE (TREE_TYPE (fndecl)) == METHOD_TYPE) + /* Give up on overloaded operator new. */ + return false; + + if (!is_named_call_p (fndecl, "operator new", call, 2) + && !is_named_call_p (fndecl, "operator new []", call, 2)) + return false; + + /* We must distinguish between an allocating non-throwing new + and a non-allocating new. + + The former might have one of the following signatures : + void* operator new (std::size_t count, const std::nothrow_t& tag); + void* operator new[] (std::size_t count, const std::nothrow_t& tag); + Whereas a placement new would take a pointer. */ + tree arg1_type = TREE_CHAIN (TYPE_ARG_TYPES (TREE_TYPE (fndecl))); + return TREE_CODE (TREE_VALUE (arg1_type)) == POINTER_TYPE; +} + namespace ana { /* Implementations of specific functions. */ @@ -46,7 +78,11 @@ class kf_operator_new : public known_function public: bool matches_call_types_p (const call_details &cd) const final override { - return cd.num_args () == 1; + return (cd.num_args () == 1 + && cd.arg_is_size_p (0)) + || (cd.num_args () == 2 + && cd.arg_is_size_p (0) + && POINTER_TYPE_P (cd.get_arg_type (1))); } void impl_call_pre (const call_details &cd) const final override @@ -54,28 +90,74 @@ public: region_model *model = cd.get_model (); region_model_manager *mgr = cd.get_manager (); const svalue *size_sval = cd.get_arg_svalue (0); - const region *new_reg - = model->get_or_create_region_for_heap_alloc (size_sval, cd.get_ctxt ()); - if (cd.get_lhs_type ()) + region_model_context *ctxt = cd.get_ctxt (); + const gcall *call = cd.get_call_stmt (); + + /* If the call was actually a placement new, check that accessing + the buffer lhs is placed into does not result in out-of-bounds. */ + if (is_placement_new_p (call)) { - const svalue *ptr_sval - = mgr->get_ptr_svalue (cd.get_lhs_type (), new_reg); - cd.maybe_set_lhs (ptr_sval); + const region *ptr_reg = cd.deref_ptr_arg (1); + if (ptr_reg && cd.get_lhs_type ()) + { + const svalue *num_bytes_sval = cd.get_arg_svalue (0); + const region *sized_new_reg + = mgr->get_sized_region (ptr_reg, + cd.get_lhs_type (), + num_bytes_sval); + model->check_region_for_write (sized_new_reg, + nullptr, + ctxt); + const svalue *ptr_sval + = mgr->get_ptr_svalue (cd.get_lhs_type (), sized_new_reg); + cd.maybe_set_lhs (ptr_sval); + } + } + /* If the call is an allocating new, then create a heap allocated + region. */ + else + { + const region *new_reg + = model->get_or_create_region_for_heap_alloc (size_sval, ctxt); + if (cd.get_lhs_type ()) + { + const svalue *ptr_sval + = mgr->get_ptr_svalue (cd.get_lhs_type (), new_reg); + cd.maybe_set_lhs (ptr_sval); + } + } + } + + void impl_call_post (const call_details &cd) const final override + { + region_model *model = cd.get_model (); + region_model_manager *mgr = cd.get_manager (); + tree callee_fndecl = cd.get_fndecl_for_call (); + region_model_context *ctxt = cd.get_ctxt (); + + /* If the call is guaranteed to return nonnull + then add a nonnull constraint to the allocated region. */ + if (!TREE_NOTHROW (callee_fndecl) && flag_exceptions) + { + const svalue *null_sval + = mgr->get_or_create_null_ptr (cd.get_lhs_type ()); + const svalue *result + = model->get_store_value (cd.get_lhs_region (), ctxt); + model->add_constraint (result, NE_EXPR, null_sval, ctxt); } } }; -/* Handler for "operator delete", both the sized and unsized variants - (2 arguments and 1 argument respectively), and for "operator delete []" */ +/* Handler for "operator delete" and for "operator delete []", + both the sized and unsized variants + (2 arguments and 1 argument respectively). */ class kf_operator_delete : public known_function { public: - kf_operator_delete (unsigned num_args) : m_num_args (num_args) {} - bool matches_call_types_p (const call_details &cd) const final override { - return cd.num_args () == m_num_args; + return cd.num_args () == 1 or cd.num_args () == 2; } void impl_call_post (const call_details &cd) const final override @@ -86,12 +168,11 @@ public: { /* If the ptr points to an underlying heap region, delete it, poisoning pointers. */ - model->unbind_region_and_descendents (freed_reg, POISON_KIND_FREED); + model->unbind_region_and_descendents (freed_reg, + POISON_KIND_DELETED); } } -private: - unsigned m_num_args; }; /* Populate KFM with instances of known functions relating to C++. */ @@ -101,9 +182,8 @@ register_known_functions_lang_cp (known_function_manager &kfm) { kfm.add ("operator new", make_unique<kf_operator_new> ()); kfm.add ("operator new []", make_unique<kf_operator_new> ()); - kfm.add ("operator delete", make_unique<kf_operator_delete> (1)); - kfm.add ("operator delete", make_unique<kf_operator_delete> (2)); - kfm.add ("operator delete []", make_unique<kf_operator_delete> (1)); + kfm.add ("operator delete", make_unique<kf_operator_delete> ()); + kfm.add ("operator delete []", make_unique<kf_operator_delete> ()); } } // namespace ana diff --git a/gcc/analyzer/region-model.cc b/gcc/analyzer/region-model.cc index eb01175..82bc3b2 100644 --- a/gcc/analyzer/region-model.cc +++ b/gcc/analyzer/region-model.cc @@ -502,6 +502,7 @@ public: case POISON_KIND_UNINIT: return OPT_Wanalyzer_use_of_uninitialized_value; case POISON_KIND_FREED: + case POISON_KIND_DELETED: return OPT_Wanalyzer_use_after_free; case POISON_KIND_POPPED_STACK: return OPT_Wanalyzer_use_of_pointer_in_stale_stack_frame; @@ -534,6 +535,15 @@ public: m_expr); } break; + case POISON_KIND_DELETED: + { + diagnostic_metadata m; + m.add_cwe (416); /* "CWE-416: Use After Free". */ + return warning_meta (rich_loc, m, get_controlling_option (), + "use after %<delete%> of %qE", + m_expr); + } + break; case POISON_KIND_POPPED_STACK: { /* TODO: which CWE? */ @@ -558,6 +568,9 @@ public: case POISON_KIND_FREED: return ev.formatted_print ("use after %<free%> of %qE here", m_expr); + case POISON_KIND_DELETED: + return ev.formatted_print ("use after %<delete%> of %qE here", + m_expr); case POISON_KIND_POPPED_STACK: return ev.formatted_print ("dereferencing pointer %qE to within stale stack frame", @@ -4194,6 +4207,29 @@ region_model::eval_condition (const svalue *lhs, } } + /* Attempt to unwrap cast if there is one, and the types match. */ + tree lhs_type = lhs->get_type (); + tree rhs_type = rhs->get_type (); + if (lhs_type && rhs_type) + { + const unaryop_svalue *lhs_un_op = dyn_cast <const unaryop_svalue *> (lhs); + const unaryop_svalue *rhs_un_op = dyn_cast <const unaryop_svalue *> (rhs); + if (lhs_un_op && CONVERT_EXPR_CODE_P (lhs_un_op->get_op ()) + && rhs_un_op && CONVERT_EXPR_CODE_P (rhs_un_op->get_op ()) + && lhs_type == rhs_type) + return eval_condition (lhs_un_op->get_arg (), + op, + rhs_un_op->get_arg ()); + + else if (lhs_un_op && CONVERT_EXPR_CODE_P (lhs_un_op->get_op ()) + && lhs_type == rhs_type) + return eval_condition (lhs_un_op->get_arg (), op, rhs); + + else if (rhs_un_op && CONVERT_EXPR_CODE_P (rhs_un_op->get_op ()) + && lhs_type == rhs_type) + return eval_condition (lhs, op, rhs_un_op->get_arg ()); + } + /* Otherwise, try constraints. Cast to const to ensure we don't change the constraint_manager as we do this (e.g. by creating equivalence classes). */ diff --git a/gcc/analyzer/sm-malloc.cc b/gcc/analyzer/sm-malloc.cc index 2ff777d..5af6544 100644 --- a/gcc/analyzer/sm-malloc.cc +++ b/gcc/analyzer/sm-malloc.cc @@ -759,7 +759,7 @@ public: override { if (change.m_old_state == m_sm.get_start_state () - && unchecked_p (change.m_new_state)) + && (unchecked_p (change.m_new_state) || nonnull_p (change.m_new_state))) // TODO: verify that it's the allocation stmt, not a copy return label_text::borrow ("allocated here"); if (unchecked_p (change.m_old_state) @@ -1179,6 +1179,21 @@ public: { return ev.formatted_print ("dereference of NULL %qE", ev.m_expr); } + + /* Implementation of pending_diagnostic::supercedes_p for + null-deref. + + We want null-deref to supercede use-of-unitialized-value, + so that if we have these at the same stmt, we don't emit + a use-of-uninitialized, just the null-deref. */ + + bool supercedes_p (const pending_diagnostic &other) const final override + { + if (other.use_of_uninit_p ()) + return true; + + return false; + } }; /* Concrete subclass for describing passing a NULL value to a @@ -1915,12 +1930,20 @@ malloc_state_machine::on_stmt (sm_context *sm_ctxt, return true; } - if (is_named_call_p (callee_fndecl, "operator new", call, 1)) - on_allocator_call (sm_ctxt, call, &m_scalar_delete); - else if (is_named_call_p (callee_fndecl, "operator new []", call, 1)) - on_allocator_call (sm_ctxt, call, &m_vector_delete); - else if (is_named_call_p (callee_fndecl, "operator delete", call, 1) - || is_named_call_p (callee_fndecl, "operator delete", call, 2)) + if (!is_placement_new_p (call)) + { + bool returns_nonnull = !TREE_NOTHROW (callee_fndecl) + && flag_exceptions; + if (is_named_call_p (callee_fndecl, "operator new")) + on_allocator_call (sm_ctxt, call, + &m_scalar_delete, returns_nonnull); + else if (is_named_call_p (callee_fndecl, "operator new []")) + on_allocator_call (sm_ctxt, call, + &m_vector_delete, returns_nonnull); + } + + if (is_named_call_p (callee_fndecl, "operator delete", call, 1) + || is_named_call_p (callee_fndecl, "operator delete", call, 2)) { on_deallocator_call (sm_ctxt, node, call, &m_scalar_delete.m_deallocator, 0); diff --git a/gcc/analyzer/svalue.cc b/gcc/analyzer/svalue.cc index 064627f..35eb830 100644 --- a/gcc/analyzer/svalue.cc +++ b/gcc/analyzer/svalue.cc @@ -970,6 +970,8 @@ poison_kind_to_str (enum poison_kind kind) return "uninit"; case POISON_KIND_FREED: return "freed"; + case POISON_KIND_DELETED: + return "deleted"; case POISON_KIND_POPPED_STACK: return "popped stack"; } diff --git a/gcc/analyzer/svalue.h b/gcc/analyzer/svalue.h index 5492b1e..263a0d7 100644 --- a/gcc/analyzer/svalue.h +++ b/gcc/analyzer/svalue.h @@ -350,6 +350,9 @@ enum poison_kind /* For use to describe freed memory. */ POISON_KIND_FREED, + /* For use to describe deleted memory. */ + POISON_KIND_DELETED, + /* For use on pointers to regions within popped stack frames. */ POISON_KIND_POPPED_STACK }; |