[PATCH] analyzer: add dynamic_cast support [PR110578]
Egas Ribeiro <[email protected]> Sat, 1 Aug 2026 13:14:24 +0100
| Newsgroups | gmane.comp.gcc.patches |
|---|---|
| Message-ID | <[email protected]> |
Bootstrapped/regtested on x86_64-pc-linux-gnu. I cc'd Jakub and Jason so I could get input on my test coverage and on the implementation of evaluate_dyncast. Everything is done on GIMPLE other than some calls in impl_call_post, so its effectively just an implementation of static evaluation of dynamic_cast but relying on the analyzer region/store model to get some information. As I mentioned to Jakub on IRC, I found that on some test cases cxx_eval_dynamic_cast wasn't agreeing with the runtime dynamic_cast, so I also opened a BZ for that: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126510 My implementation with the analyzer matches the runtime for that case. There was no particular way for me to reuse the methods that cxx_eval_dynamic_cast uses because not only are they in the frontend (so I can't access them) but also they consider more c++ semantics than I have to for what I am doing, so my solution was to reimplement the rules as per the c++ standard. All things considered the implementation isn't too extensive. Would like some feedback from David whether this is a good approach here. Thanks, Egas -- >8 -- The analyzer doesn't currently understand dynamic_cast calls. This means that the returned object from a call is always opaque, and thus we miss a few classes of bugs such as dereferencing the result of a failed cast. This patch implements a new known_function for dynamic_cast, and sets the result of the callbased on the rules of [expr.dynamic.cast]. The implementation effectively evaluates the results of a dynamic_cast cast call statically, similarly to what cxx_eval_dynamic_cast does in the frontend, but without using frontend methods and trees. It tries to derive the target object from the argumments passed to __dynamic_cast calls by traversing BINFOs based on the rules of [expr.dynamic.cast] and then calculating the relative offsets of the resulting object from the BINFOs we found. get_vtable_from_obj was also added so we can reuse the logic for deriving the vtable from objects added with virtual function support. gcc/analyzer/ChangeLog: * analyzer.cc (is_fndecl_in_toplevel_namespace_p): New function, factored out of... (is_std_function_p): ...here. Call it. (is_cxxabi_function_p): New function. * common.h (is_cxxabi_function_p): New decl. * kf-lang-cp.cc: Include "cgraph.h" and "ipa-utils.h". (struct dyncast_subobject): New struct. (get_type_from_tinfo_arg): New function. (lookup_binfo_at_same_offset): New function. (lookup_subobject_matches): New function. (evaluate_dyncast): New function. (class kf_dynamic_cast): New class. (register_known_functions_lang_cp): Register "__dynamic_cast". * known-function-manager.cc (known_function_manager::get_match): Also match functions declared in namespace __cxxabiv1. * region-model.cc (region_model::get_vtable_from_obj): New function, factored out of... (region_model::get_fndecl_for_virtual_call): ...here. Call it. Update comment. * region-model.h (region_model::get_vtable_from_obj): New decl. gcc/testsuite/ChangeLog: * g++.dg/analyzer/dyncast-1.C: Rewrite to cover [expr.dynamic.cast]/9.1 and /9.2 over public, non-virtual inheritance. * g++.dg/analyzer/dyncast-2.C: New test. * g++.dg/analyzer/dyncast-3.C: New test. * g++.dg/analyzer/dyncast-4.C: New test. * g++.dg/analyzer/dyncast-5.C: New test. Signed-off-by: Egas Ribeiro <[email protected]> --- gcc/analyzer/analyzer.cc | 26 +- gcc/analyzer/common.h | 1 + gcc/analyzer/kf-lang-cp.cc | 286 ++++++++++++++++++++++ gcc/analyzer/known-function-manager.cc | 10 +- gcc/analyzer/region-model.cc | 79 ++++-- gcc/analyzer/region-model.h | 4 + gcc/testsuite/g++.dg/analyzer/dyncast-1.C | 78 ++++-- gcc/testsuite/g++.dg/analyzer/dyncast-2.C | 57 +++++ gcc/testsuite/g++.dg/analyzer/dyncast-3.C | 50 ++++ gcc/testsuite/g++.dg/analyzer/dyncast-4.C | 83 +++++++ gcc/testsuite/g++.dg/analyzer/dyncast-5.C | 30 +++ 11 files changed, 657 insertions(+), 47 deletions(-) create mode 100644 gcc/testsuite/g++.dg/analyzer/dyncast-2.C create mode 100644 gcc/testsuite/g++.dg/analyzer/dyncast-3.C create mode 100644 gcc/testsuite/g++.dg/analyzer/dyncast-4.C create mode 100644 gcc/testsuite/g++.dg/analyzer/dyncast-5.C diff --git a/gcc/analyzer/analyzer.cc b/gcc/analyzer/analyzer.cc index f58cfc93fc7..1425e913379 100644 --- a/gcc/analyzer/analyzer.cc +++ b/gcc/analyzer/analyzer.cc @@ -349,27 +349,47 @@ is_named_call_p (const_tree fndecl, const char *funcname) return 0 == strcmp (tname, funcname); } -/* Return true if FNDECL is within the namespace "std". +/* Return true if FNDECL is declared directly within a top-level + namespace named NS_NAME (e.g. "std" or "__cxxabiv1"). Compare with cp/typeck.cc: decl_in_std_namespace_p, but this doesn't rely on being the C++ FE (or handle inline namespaces inside of std). */ bool -is_std_function_p (const_tree fndecl) +is_fndecl_in_toplevel_namespace_p (const_tree fndecl, const char *ns_name) { tree name_decl = DECL_NAME (fndecl); if (!name_decl) return false; + if (!DECL_CONTEXT (fndecl)) return false; if (TREE_CODE (DECL_CONTEXT (fndecl)) != NAMESPACE_DECL) return false; tree ns = DECL_CONTEXT (fndecl); + /* Require the namespace itself to be at top level. */ if (!(DECL_CONTEXT (ns) == NULL_TREE || TREE_CODE (DECL_CONTEXT (ns)) == TRANSLATION_UNIT_DECL)) return false; if (!DECL_NAME (ns)) return false; - return id_equal ("std", DECL_NAME (ns)); + + return id_equal (ns_name, DECL_NAME (ns)); +} + +/* Return true if FNDECL is within the namespace "std". */ + +bool +is_std_function_p (const_tree fndecl) +{ + return is_fndecl_in_toplevel_namespace_p (fndecl, "std"); +} + +/* Return true if FNDECL is within the namespace "__cxxabiv1". */ + +bool +is_cxxabi_function_p (const_tree fndecl) +{ + return is_fndecl_in_toplevel_namespace_p (fndecl, "__cxxabiv1"); } /* Like is_named_call_p, but look for std::FUNCNAME. */ diff --git a/gcc/analyzer/common.h b/gcc/analyzer/common.h index 8ce1475bc5e..05452ef02ed 100644 --- a/gcc/analyzer/common.h +++ b/gcc/analyzer/common.h @@ -544,6 +544,7 @@ extern bool is_named_call_p (const_tree fndecl, const char *funcname); extern bool is_named_call_p (const_tree fndecl, const char *funcname, const gcall &call, unsigned int num_args); extern bool is_std_function_p (const_tree fndecl); +extern bool is_cxxabi_function_p (const_tree fndecl); extern bool is_std_named_call_p (const_tree fndecl, const char *funcname); extern bool is_std_named_call_p (const_tree fndecl, const char *funcname, const gcall &call, unsigned int num_args); diff --git a/gcc/analyzer/kf-lang-cp.cc b/gcc/analyzer/kf-lang-cp.cc index 5d57bcc5fcb..1b0dfc660e6 100644 --- a/gcc/analyzer/kf-lang-cp.cc +++ b/gcc/analyzer/kf-lang-cp.cc @@ -19,6 +19,8 @@ along with GCC; see the file COPYING3. If not see <http://www.gnu.org/licenses/>. */ #include "analyzer/common.h" +#include "cgraph.h" +#include "ipa-utils.h" #include "diagnostic.h" @@ -63,6 +65,287 @@ namespace ana { /* Implementations of specific functions. */ +/* Handler for __dynamic_cast. */ + +/* A candidate TYPE subobject found on one inheritance path. */ + +struct dyncast_subobject +{ + tree binfo; /* the BINFO that represents our subobject. + NULL_TREE if failed. */ + bool accessible; /* Every edge from the root was public. */ +}; + +/* Recover the class type from a type_info argument of __dynamic_cast, expected + to be &_ZTIxxx. The C++ FE sets TREE_TYPE on the tinfo decl's DECL_NAME + identifier. __dynamic_cast is callable directly, so a runtime tinfo pointer + (an SSA name) can exist. Return NULL_TREE on any shape mismatch. */ + +static tree +get_type_from_tinfo_arg (tree arg) +{ + if (!arg || TREE_CODE (arg) != ADDR_EXPR) + return NULL_TREE; + tree tinfo_decl = TREE_OPERAND (arg, 0); + if (!DECL_P (tinfo_decl) || !DECL_NAME (tinfo_decl)) + return NULL_TREE; + tree type = TREE_TYPE (DECL_NAME (tinfo_decl)); + if (!type || !RECORD_OR_UNION_TYPE_P (type)) + return NULL_TREE; + return TYPE_MAIN_VARIANT (type); +} + +/* Find the sub-BINFO of BINFO that has type TARGET_TYPE and sits at the same + address as BINFO itself (i.e. at relative offset 0) by descending through + every base at that same (absolute) offset. */ + +static tree +lookup_binfo_at_same_offset (tree binfo, tree target_type) +{ + if (types_same_for_odr (BINFO_TYPE (binfo), target_type)) + return binfo; + + tree offset = BINFO_OFFSET (binfo); + tree base_binfo; + for (unsigned i = 0; BINFO_BASE_ITERATE (binfo, i, base_binfo); i++) + if (tree_int_cst_equal (BINFO_OFFSET (base_binfo), offset)) + if (tree found = lookup_binfo_at_same_offset (base_binfo, target_type)) + return found; + return NULL_TREE; +} + +/* Look recursively for a BINFO that matches our DST_TYPE. This method might + find multiple matches. If none is found, matches won't be changed. + + Even if one path is private, it is still ambiguous according to the + definition, so that case still counts as having multiple matches. That + means we ignore access specifiers when searching bases. + + Morally virtual matches of the same type under different virtual ancestors + are distinct subobjects, i.e: + + class B0 {}; + class V1 : B0 {}; class V2 : B0 {}; + class B2 : virtual V1 {}; class B4 : virtual V2 {}; + class MD : B2, B3 {}; + + Here, each B0 is a different subobject, so we must account for this case + when checking virtual inheritance. We compare the BINFO_OFFSET of all the + BINFOs that match (the offset is relative to our most-derived object) to + decide if they belong to the same suboject. Note that if it is at the same + BINFO_OFFSET and has the same TREE_TYPE, it must necessarily be the same + subobject. */ + +static void +lookup_subobject_matches (const_tree target_type, const dyncast_subobject match, + auto_vec<dyncast_subobject> &matches) +{ + if (types_same_for_odr (BINFO_TYPE (match.binfo), target_type)) + { + /* Check if we had already found this particular subobject. */ + tree match_offset = BINFO_OFFSET (match.binfo); + for (auto &subobject : matches) + if (tree_int_cst_equal (BINFO_OFFSET (subobject.binfo), match_offset)) + { + subobject.accessible |= match.accessible; + return; /* We are adding the same subobject, so skip it. */ + } + matches.safe_push (match); + return; + } + tree parent_binfo = match.binfo; + tree base_binfo; + for (unsigned i = 0; BINFO_BASE_ITERATE (parent_binfo, i, base_binfo); i++) + { + dyncast_subobject child = match; + child.binfo = base_binfo; + /* If BINFO_BASE_ACCESSES is not present, public access is implied. */ + child.accessible + &= !BINFO_BASE_ACCESSES (parent_binfo) + || BINFO_BASE_ACCESS (parent_binfo, i) == access_public_node; + /* Check if the next binfo might be our DST_TYPE binfo recursively. */ + lookup_subobject_matches (target_type, child, matches); + } +} + +/* We implement the runtime check rules as per [expr.dynamic.cast]9. + As a general overview, those rules state: + [expr.dynamic.cast]/9.1: Does SRC_OBJ point to a public base subobject of + a DST_TYPE object? And is there only one DST_TYPE object derived from + SRC_OBJ? + We expect the hierarchy to be something like: + SRC_TYPE -> ... -> DST_TYPE -> ... -> MD_TYPE. + + [expr.dynamic.cast]/9.2: Otherwise, does SRC_OBJ point to a public base + subobject of a MDTYPE object? And is DST_TYPE an unambiguous and public + base of MDTYPE? + We expect the hierarchy to be something like: + MD_TYPE -> ... -> DST_TYPE + + [expr.dynamic.cast]/9.3: Otherwise, the runtime check fails. */ + +static dyncast_subobject +evaluate_dyncast (tree dst_type, tree md_binfo, tree src_binfo) +{ + dyncast_subobject no_base_match = {.binfo = NULL_TREE, .accessible = false}; + + /* Start by assuming the path will be public. */ + auto_vec<dyncast_subobject> dst_matches; + dyncast_subobject md_subobject = {.binfo = md_binfo, .accessible = true}; + lookup_subobject_matches (dst_type, md_subobject, dst_matches); + + /* Per [expr.dynamic.cast]/9.1: + Only one object of DST_TYPE can be derived from this SRC_OBJ and + The path from DST -> SRC must be public. */ + tree src_offset = BINFO_OFFSET (src_binfo); + tree src_type = BINFO_TYPE (src_binfo); + + auto_vec<dyncast_subobject> clause1_matches; + for (const auto &dst_subobj : dst_matches) + { + auto_vec<dyncast_subobject> src_matches; + dyncast_subobject from_dst = {dst_subobj.binfo, /* accessible = */ true}; + lookup_subobject_matches (src_type, from_dst, src_matches); + /* Only keep matches that derive from this src subobject. */ + for (const auto &src_subobj : src_matches) + if (tree_int_cst_equal (BINFO_OFFSET (src_subobj.binfo), src_offset)) + /* Keep dst BINFO but save whether SRC is a public base of DST. */ + clause1_matches.safe_push ({dst_subobj.binfo, src_subobj.accessible}); + } + if (clause1_matches.length () == 1 && clause1_matches[0].accessible) + return clause1_matches[0]; /* No ambiguity, only one public match. */ + + /* No match or the match we found was private. Try clause 2. */ + + /* Otherwise, per [expr.dynamic.cast]/9.2: + Require a public path from MD_OBJ -> SRC_OBJ and + Require that DST_TYPE is an unambiguous and public base of MD_TYPE. */ + if (dst_matches.length () == 1 && dst_matches[0].accessible) + { + auto_vec<dyncast_subobject> src_matches; + lookup_subobject_matches (src_type, md_subobject, src_matches); + /* Find any public path from MD_OBJ -> SRC_OBJ. */ + for (const auto &src_subobj : src_matches) + if (src_subobj.accessible) + return dst_matches[0]; /* Found a public match. */ + } + return no_base_match; /* No match or the match we found was private. */ +} + +class kf_dynamic_cast : public known_function +{ +public: + bool matches_call_types_p (const call_details &cd) const final override + { + /* A call will look something like: + Derived *d; + d = __dynamic_cast ((Base*) b, &_ZTI1Base, &_ZTI1Derived, 8); */ + return (cd.num_args () == 4 && POINTER_TYPE_P (cd.get_arg_type (0)) + && POINTER_TYPE_P (cd.get_arg_type (1)) + && POINTER_TYPE_P (cd.get_arg_type (2)) + && INTEGRAL_TYPE_P (cd.get_arg_type (3))); + } + void impl_call_post (const call_details &cd) const final override + { + region_model *model = cd.get_model (); + region_model_manager *mgr = cd.get_manager (); + + cd.set_any_lhs_with_defaults (); + + tree dst_ptr_type = cd.get_lhs_type (); + if (!dst_ptr_type) + return; + + /* Recover the class types from the tinfo args. */ + tree src_type = get_type_from_tinfo_arg (cd.get_arg_tree (1)); + tree dst_type = get_type_from_tinfo_arg (cd.get_arg_tree (2)); + if (!src_type || !dst_type) + return; + + /* Read the vptr binding of the object; VPTR_OFF selects the + sub-vtable within the vtable decl, so it identifies which subobject's + vptr we read. */ + tree src_obj = cd.get_arg_tree (0); + unsigned HOST_WIDE_INT vptr_off; + tree vtable + = model->get_vtable_from_obj (src_obj, src_type, mgr, nullptr, &vptr_off); + /* The class the vtable belongs to is the dynamic (most-derived) type of + the object. VTABLE is whatever decl the vptr slot happened to point at, + so check it really is a vtable. */ + if (!vtable || !VAR_P (vtable) || !DECL_VIRTUAL_P (vtable)) + return; + tree mdtype = DECL_CONTEXT (vtable); + if (!mdtype || !RECORD_OR_UNION_TYPE_P (mdtype)) + return; + tree md_binfo = TYPE_BINFO (mdtype); + if (!md_binfo) + return; + + /* Given the value stored to SRC_OBJ's vtpr field (&_ZTV* + offset), find + which subobject of this hierarchy would have this value written into its + vptr. */ + tree vtable_binfo + = subbinfo_with_vtable_at_offset (md_binfo, vptr_off, vtable); + if (!vtable_binfo) + return; + /* With a shared primary-base vtable the owning binfo may be an enclosing + type. Consider: + class A {}; + class B {}; + class C : B {}; + class D : A, C {}; + Here the vptr value stored in the B-subobject's slot is owned by the C + binfo (C's sub-vtable group), and a lookup with B's vptr value returns + the C binfo, not B's (BINFO_VTABLE is only set on the owner, cf. + ipa-devirt.cc:61). In this case, the src subobject sits on its primary + chain (relative offset 0), which lookup_binfo_at_same_offset finds. */ + tree src_binfo = lookup_binfo_at_same_offset (vtable_binfo, src_type); + if (!src_binfo) + return; + + dyncast_subobject dst_match + = evaluate_dyncast (dst_type, md_binfo, src_binfo); + + if (!dst_match.binfo) + { /* [expr.dynamic.cast]/9.3: Otherwise, the runtime check failed. */ + cd.maybe_set_lhs (mgr->get_or_create_null_ptr (dst_ptr_type)); + return; + } + + /* Build a pointer to the dst subobject. Work in byte offsets relative to + SRC_REG's base region; we never need a region for the mdtype object + itself, only its start offset, recovered from where the src subobject + sits within MDTYPE. */ + const region *src_reg = cd.deref_ptr_arg (0); + region_offset off = src_reg->get_offset (mgr); + if (!off.concrete_p ()) + return; /* Bail, leave lhs conjured. */ + byte_offset_t src_obj_start; + if (!off.get_concrete_byte_offset (&src_obj_start)) + return; + + HOST_WIDE_INT src_off_in_md = tree_to_shwi (BINFO_OFFSET (src_binfo)); + HOST_WIDE_INT dst_off_in_md = tree_to_shwi (BINFO_OFFSET (dst_match.binfo)); + HOST_WIDE_INT md_start_in_base = src_obj_start.to_shwi () - src_off_in_md; + if (md_start_in_base < 0) + return; /* Layout disagreement between the store and the binfo data; + bail rather than build a negative-offset region. */ + HOST_WIDE_INT dst_off_in_base = md_start_in_base + dst_off_in_md; + + /* BASE_REG is the outermost region, not necessarily the mdtype + object (it might sit at a nonzero offset inside BASE_REG, e.g. as an + array element or a member subobject). The store binds values by byte + ranges within a base region, so a concrete offset_region aliases the + FE's field-path accesses to the same bytes. */ + const region *base_reg = off.get_base_region (); + const svalue *dst_off_sval + = mgr->get_or_create_int_cst (size_type_node, dst_off_in_base); + const region *dst_reg + = mgr->get_offset_region (base_reg, dst_type, dst_off_sval); + cd.maybe_set_lhs (mgr->get_ptr_svalue (dst_ptr_type, dst_reg)); + } +}; + /* Handler for "operator new" and "operator new []". */ class kf_operator_new : public known_function @@ -371,6 +654,9 @@ register_known_functions_lang_cp (known_function_manager &kfm) kfm.add ("__cxa_end_catch", std::make_unique<kf_cxa_end_catch> ()); kfm.add ("__cxa_call_unexpected", std::make_unique<kf_cxa_call_unexpected> ()); + + /* TODO: [add Itanium C++ ABI mention of __dynamic_cast here] */ + kfm.add ("__dynamic_cast", std::make_unique<kf_dynamic_cast> ()); } } // namespace ana diff --git a/gcc/analyzer/known-function-manager.cc b/gcc/analyzer/known-function-manager.cc index fcc9a618d15..679fe1a7015 100644 --- a/gcc/analyzer/known-function-manager.cc +++ b/gcc/analyzer/known-function-manager.cc @@ -120,9 +120,13 @@ known_function_manager::get_match (tree fndecl, const call_details &cd) const return nullptr; } - if (DECL_CONTEXT (fndecl) - && TREE_CODE (DECL_CONTEXT (fndecl)) != TRANSLATION_UNIT_DECL) - return nullptr; + /* Only match functions declared at global scope, or within namespace + __cxxabiv1 (e.g. __dynamic_cast). */ + if (!is_cxxabi_function_p (fndecl)) + if (DECL_CONTEXT (fndecl) + && TREE_CODE (DECL_CONTEXT (fndecl)) != TRANSLATION_UNIT_DECL) + return nullptr; + if (tree identifier = DECL_NAME (fndecl)) if (const known_function *candidate = get_by_identifier (identifier)) if (candidate->matches_call_types_p (cd)) diff --git a/gcc/analyzer/region-model.cc b/gcc/analyzer/region-model.cc index 8addf1d9a07..caa166497df 100644 --- a/gcc/analyzer/region-model.cc +++ b/gcc/analyzer/region-model.cc @@ -6490,25 +6490,23 @@ region_model::can_merge_with_p (const region_model &other_model, return true; } -/* Attempt to get the fndecl for a virtual call via OBJ_TYPE_REF, or - NULL_TREE if it can't be resolved. +/* Recover the vtable OBJ's vptr (for OBJ_TYPE) actually points to, plus the + byte offset into it, so callers can recover the most-derived type from the + vtable's DECL_CONTEXT and BINFO. - Reads the value bound to the object's vptr field (OBJ_TYPE_REF_OBJECT's - vfield). - If that value has the form "&vtable_decl + constant" (a region_svalue for a - _ZTV* decl plus a byte offset), recover the vtable decl and offset and use - gimple_get_virt_method_for_vtable, together with OBJ_TYPE_REF_TOKEN, to look - up the concrete fndecl in the vtable's initializer. - - Relies on the store having bound the vptr field to the _ZTV* instance, so no - separate modeling of the object's dynamic type is needed. */ + Only recognizes the shape we model for a vptr store, "vptr_field = + &vtable_decl + offset", i.e. a POINTER_PLUS_EXPR binop_svalue of a + region_svalue for the _ZTV* decl and a constant offset. The offset is + nonzero when this vptr slot belongs to a non-primary base's own sub-vtable + group within the same decl; we return it via OUT for callers that need to + index into the vtable (e.g. gimple_get_virt_method_for_vtable). */ tree -region_model::get_fndecl_for_virtual_call (const_tree obj_type_ref, - region_model_context *ctxt) +region_model::get_vtable_from_obj (tree obj, tree obj_type, + region_model_manager *mgr, + region_model_context *ctxt, + unsigned HOST_WIDE_INT *out) const { - tree obj = OBJ_TYPE_REF_OBJECT (obj_type_ref); - tree obj_type = obj_type_ref_class (obj_type_ref); if (!obj_type) return NULL_TREE; tree vfield = TYPE_VFIELD (obj_type); @@ -6517,7 +6515,7 @@ region_model::get_fndecl_for_virtual_call (const_tree obj_type_ref, const svalue *obj_sval = get_rvalue (obj, ctxt); const region *obj_reg = deref_rvalue (obj_sval, obj, ctxt); - const region *vptr_reg = m_mgr->get_field_region (obj_reg, vfield); + const region *vptr_reg = mgr->get_field_region (obj_reg, vfield); const svalue *vptr_sval = get_store_value (vptr_reg, ctxt); while (const svalue *cast = vptr_sval->maybe_undo_cast ()) @@ -6527,20 +6525,55 @@ region_model::get_fndecl_for_virtual_call (const_tree obj_type_ref, if (!b || b->get_op () != POINTER_PLUS_EXPR) return NULL_TREE; - vptr_sval = b->get_arg0 (); - const svalue *offset_sval = b->get_arg1 (); - - tree offset_const = offset_sval->maybe_get_constant (); - if (!offset_const || TREE_CODE (offset_const) != INTEGER_CST) - return NULL_TREE; - unsigned HOST_WIDE_INT offset = tree_to_uhwi (offset_const); + if (out) + { + const svalue *offset_sval = b->get_arg1 (); + tree offset_const = offset_sval->maybe_get_constant (); + if (!offset_const || TREE_CODE (offset_const) != INTEGER_CST) + return NULL_TREE; + *out = tree_to_uhwi (offset_const); + } + vptr_sval = b->get_arg0 (); const region_svalue *vptr = vptr_sval->dyn_cast_region_svalue (); /* Give up if we have a conjured vptr. */ if (!vptr) return NULL_TREE; tree vtable = vptr->get_pointee ()->maybe_get_decl (); + return vtable; +} + +/* Attempt to get the fndecl for a virtual call via OBJ_TYPE_REF, or + NULL_TREE if it can't be resolved. + + A virtual call's callee is a GIMPLE OBJ_TYPE_REF: + OBJ_TYPE_REF(EXPR; (TYPE)OBJECT->TOKEN) + EXPR is the function pointer actually loaded and called; OBJECT, TYPE and + TOKEN are devirtualization metadata, not needed to perform the call itself. + OBJECT is the "this" pointer, TYPE its static type, TOKEN the vtable slot + index. We ignore EXPR and instead resolve TOKEN against OBJECT's modeled + dynamic type rather than its static TYPE. + + Reads the value bound to the object's vptr field (OBJ_TYPE_REF_OBJECT's + vfield). + If that value has the form "&vtable_decl + constant" (a region_svalue for a + _ZTV* decl plus a byte offset), recover the vtable decl and offset and use + gimple_get_virt_method_for_vtable, together with OBJ_TYPE_REF_TOKEN, to look + up the concrete fndecl in the vtable's initializer. + + Relies on the store having bound the vptr field to the _ZTV* instance, so no + separate modeling of the object's dynamic type is needed. */ + +tree +region_model::get_fndecl_for_virtual_call (const_tree obj_type_ref, + region_model_context *ctxt) +{ + tree obj = OBJ_TYPE_REF_OBJECT (obj_type_ref); + tree obj_type = obj_type_ref_class (obj_type_ref); + + unsigned HOST_WIDE_INT offset; + tree vtable = get_vtable_from_obj (obj, obj_type, m_mgr, ctxt, &offset); if (!vtable) return NULL_TREE; diff --git a/gcc/analyzer/region-model.h b/gcc/analyzer/region-model.h index d1e2b014d0e..22a57a84735 100644 --- a/gcc/analyzer/region-model.h +++ b/gcc/analyzer/region-model.h @@ -512,6 +512,10 @@ class region_model tree get_fndecl_for_virtual_call (const_tree fn_ptr, region_model_context *ctxt); + tree get_vtable_from_obj (tree obj, tree obj_type, region_model_manager *mgr, + region_model_context *ctxt, + unsigned HOST_WIDE_INT *out = nullptr) const; + void get_regions_for_current_frame (auto_vec<const decl_region *> *out) const; static void append_regions_cb (const region *base_reg, struct append_regions_cb_data *data); diff --git a/gcc/testsuite/g++.dg/analyzer/dyncast-1.C b/gcc/testsuite/g++.dg/analyzer/dyncast-1.C index 14acb91ffaa..53d62f28027 100644 --- a/gcc/testsuite/g++.dg/analyzer/dyncast-1.C +++ b/gcc/testsuite/g++.dg/analyzer/dyncast-1.C @@ -1,21 +1,63 @@ +/* Basic runtime checks: + [expr.dynamic.cast]/9.1 and /9.2 over public, non-virtual inheritance. */ + #include "../../gcc.dg/analyzer/analyzer-decls.h" -struct base -{ - virtual ~base () {} -}; -struct sub : public base -{ - int m_field; -}; - -int -test_1 (base *p) -{ - if (sub *q = dynamic_cast <sub*> (p)) - { - __analyzer_dump_path (); // { dg-message "path" } - return q->m_field; - } - return 0; +struct A { virtual ~A () {} }; +struct B : A { }; +struct C : B { int m; }; +struct S { virtual ~S () {} }; +struct D : C, S { }; + +/* /9.1: SRC is a public base subobject of a unique DST object. */ +void test_downcast () { + C obj; + obj.m = 50; + A *a = &obj; + + __analyzer_eval (dynamic_cast<B *> (a) != NULL); /* { dg-warning "TRUE" } */ + + C *c = dynamic_cast<C *> (a); + __analyzer_eval (c != NULL); /* { dg-warning "TRUE" } */ + /* The result region must alias the FE's own field accesses. */ + __analyzer_eval (c->m == 50); /* { dg-warning "TRUE" } */ + __analyzer_eval (c == &obj); /* { dg-warning "TRUE" } */ +} + +/* The dynamic type is the SRC type itself: no DST above it. */ +void test_no_derived_object () { + A obj; + A *a = &obj; + __analyzer_eval (dynamic_cast<C *> (a) == NULL); /* { dg-warning "TRUE" } */ +} + +/* SRC sits at a nonzero offset in MDTYPE. */ +void test_from_secondary_base () { + D obj; + obj.m = 7; + S *s = &obj; + D *d = dynamic_cast<D *> (s); + __analyzer_eval (d != NULL); /* { dg-warning "TRUE" } */ + __analyzer_eval (d->m == 7); /* { dg-warning "TRUE" } */ +} + +/* /9.2: A and S are unrelated, both public bases of D. */ +void test_sidecast () { + D obj; + A *a = &obj; + S *s = dynamic_cast<S *> (a); + __analyzer_eval (s != NULL); /* { dg-warning "TRUE" } */ + __analyzer_eval (s == (S *) &obj); /* { dg-warning "TRUE" } */ +} + +/* Nothing is known about the dynamic type. */ +void test_symbolic (A *p) { + __analyzer_eval (dynamic_cast<S *> (p) == NULL); /* { dg-warning "UNKNOWN" } */ + /* { dg-warning "TRUE" "" { target *-*-* } .-1 } */ +} + +/* /6: a null operand yields null with no runtime check. */ +void test_null_operand () { + A *p = NULL; + __analyzer_eval (dynamic_cast<S *> (p) == NULL); /* { dg-warning "TRUE" } */ } diff --git a/gcc/testsuite/g++.dg/analyzer/dyncast-2.C b/gcc/testsuite/g++.dg/analyzer/dyncast-2.C new file mode 100644 index 00000000000..907635ea911 --- /dev/null +++ b/gcc/testsuite/g++.dg/analyzer/dyncast-2.C @@ -0,0 +1,57 @@ +/* Access control. + /9.1 constrains only the DST -> SRC path + /9.2 also requires SRC to be a public base subobject of MDTYPE. */ + +#include "../../gcc.dg/analyzer/analyzer-decls.h" + +struct P1 { virtual ~P1 () {} }; +struct P2 { virtual ~P2 () {} }; +struct B : private P1 { virtual ~B () {} }; +struct C { virtual ~C () {} }; +struct U { virtual ~U () {} }; +struct MD : B, C, protected P2 { }; + +void test_nonpublic_src () { + MD obj; + P1 *p1 = (P1 *) &obj; + P2 *p2 = (P2 *) &obj; + + /* /9.1 fails and /9.2's first premise fails. */ + __analyzer_eval (dynamic_cast<B *> (p1) == NULL); /* { dg-warning "TRUE" } */ + __analyzer_eval (dynamic_cast<C *> (p1) == NULL); /* { dg-warning "TRUE" } */ + __analyzer_eval (dynamic_cast<C *> (p2) == NULL); /* { dg-warning "TRUE" } */ +} + +void test_nonpublic_dst () { + MD obj; + B *b = &obj; + /* P2 is a protected, U is not a base. */ + __analyzer_eval (dynamic_cast<P2 *> (b) == NULL); /* { dg-warning "TRUE" } */ + __analyzer_eval (dynamic_cast<U *> (b) == NULL); /* { dg-warning "TRUE" } */ +} + +/* Private edge moved across the DST object. */ + +struct Base { virtual ~Base () {} }; +struct Mid : Base { }; +struct Outer : private Mid { }; +struct Mid2 : private Base { }; +struct Outer2 : Mid2 { }; + +void test_private_above_dst () { + Outer obj; + Base *b = (Base *) (Mid *) &obj; + /* /9.1 succeeds: Base is a public base of a unique Mid object. + The private Mid -> Outer edge doesn't affect /9.1. */ + __analyzer_eval (dynamic_cast<Mid *> (b) != NULL); /* { dg-warning "TRUE" } */ + /* ... but Outer itself is unreachable. */ + __analyzer_eval (dynamic_cast<Outer *> (b) == NULL); /* { dg-warning "TRUE" } */ +} + +void test_private_below_dst () { + Outer2 obj; + Base *b = (Base *) (Mid2 *) &obj; + /* /9.1 fails: no DST object has Base as a public base subobject. */ + __analyzer_eval (dynamic_cast<Mid2 *> (b) == NULL); /* { dg-warning "TRUE" } */ + __analyzer_eval (dynamic_cast<Outer2 *> (b) == NULL); /* { dg-warning "TRUE" } */ +} diff --git a/gcc/testsuite/g++.dg/analyzer/dyncast-3.C b/gcc/testsuite/g++.dg/analyzer/dyncast-3.C new file mode 100644 index 00000000000..8dce3158509 --- /dev/null +++ b/gcc/testsuite/g++.dg/analyzer/dyncast-3.C @@ -0,0 +1,50 @@ +/* Ambiguity ignores access, and /9.1 is anchored at one SRC subobject + so a repeated base need not be ambiguous there. */ + +#include "../../gcc.dg/analyzer/analyzer-decls.h" + +struct A { virtual ~A () {} }; +struct C { virtual ~C () {} }; +struct P1 : C { }; +struct P2 : C { }; +struct MDpub : A, P1, P2 { }; +struct MDpriv : A, P1, private P2 { }; + +/* /9.1 fails (A is below no C); /9.2 sees two C subobjects. */ +void test_ambiguous_dst () { + MDpub obj; + A *a = &obj; + __analyzer_eval (dynamic_cast<C *> (a) == NULL); /* { dg-warning "TRUE" } */ +} + +/* One of the two C subobjects is only reachable privately: ambiguity + ignores access, so this is still null. */ +void test_ambiguous_dst_mixed_access () { + MDpriv obj; + A *a = &obj; + __analyzer_eval (dynamic_cast<C *> (a) == NULL); /* { dg-warning "TRUE" } */ +} + +/* The SRC subobject selects which C encloses it. */ +void test_repeated_base_anchored_at_src () { + MDpub obj; + C *c = (C *) (P1 *) &obj; + + /* /9.1: exactly one P1, and one MDpub, derive from this C. */ + __analyzer_eval (dynamic_cast<P1 *> (c) != NULL); /* { dg-warning "TRUE" } */ + __analyzer_eval (dynamic_cast<MDpub *> (c) != NULL); /* { dg-warning "TRUE" } */ + + /* /9.1 fails for P2 (this C is not inside one), + but /9.2 succeeds: P2 is an unambiguous public base of MDpub. + Result is the other branch of the hierarchy. */ + __analyzer_eval (dynamic_cast<P2 *> (c) != NULL); /* { dg-warning "TRUE" } */ + __analyzer_eval (dynamic_cast<P2 *> (c) == (P2 *) &obj); /* { dg-warning "TRUE" } */ +} + +/* Same, with P2 private: /9.2 now fails. */ +void test_repeated_base_private_sibling () { + MDpriv obj; + C *c = (C *) (P1 *) &obj; + __analyzer_eval (dynamic_cast<P1 *> (c) != NULL); /* { dg-warning "TRUE" } */ + __analyzer_eval (dynamic_cast<P2 *> (c) == NULL); /* { dg-warning "TRUE" } */ +} diff --git a/gcc/testsuite/g++.dg/analyzer/dyncast-4.C b/gcc/testsuite/g++.dg/analyzer/dyncast-4.C new file mode 100644 index 00000000000..127b52b9c49 --- /dev/null +++ b/gcc/testsuite/g++.dg/analyzer/dyncast-4.C @@ -0,0 +1,83 @@ +/* Virtual bases. Before C++26, a constexpr constructor/destructor in a + class with virtual bases is rejected outright (constexpr-dynamic10.C), so + this part of /9 has no pre-C++26 counterpart in g++.dg/cpp2a. */ + +#include "../../gcc.dg/analyzer/analyzer-decls.h" + +struct A { virtual ~A () {} }; + +/* One shared V subobject, reached by two paths. */ +struct V { virtual ~V () {} int m; }; +struct L : virtual V { }; +struct R : virtual V { }; +struct Diamond : A, L, R { }; + +void test_shared_virtual_dst () { + Diamond obj; + obj.m = 5; + A *a = &obj; + V *v = dynamic_cast<V *> (a); + __analyzer_eval (v != NULL); /* { dg-warning "TRUE" } */ + __analyzer_eval (v->m == 5); /* { dg-warning "TRUE" } */ +} + +/* Only one of the two paths to the shared V is public. */ +struct LPub : public virtual V { }; +struct RPriv : private virtual V { }; +struct MixedDiamond : A, LPub, RPriv { }; + +void test_shared_virtual_dst_mixed_access () { + MixedDiamond obj; + A *a = &obj; + /* [class.access.base]: one public path suffices. */ + __analyzer_eval (dynamic_cast<V *> (a) != NULL); /* { dg-warning "TRUE" } */ +} + +/* Two distinct B0 subobjects, each under its own virtual ancestor. */ +struct B0 { virtual ~B0 () {} }; +struct V1 : B0 { }; +struct V2 : B0 { }; +struct D1 : virtual V1 { }; +struct D2 : virtual V2 { }; +struct TwoB0 : A, D1, D2 { }; + +void test_distinct_virtual_subobjects () { + TwoB0 obj; + A *a = &obj; + __analyzer_eval (dynamic_cast<B0 *> (a) == NULL); /* { dg-warning "TRUE" } */ +} + +/* SRC is a shared virtual base with two enclosing C objects: /9.1's + uniqueness clause fails, and /9.2 then finds C ambiguous. */ +struct S { virtual ~S () {} }; +struct C : virtual S { }; +struct CL : C { }; +struct CR : C { }; +struct TwoC : CL, CR { }; + +void test_virtual_src_two_enclosing_dst () { + TwoC obj; + S *s = &obj; + __analyzer_eval (dynamic_cast<C *> (s) == NULL); /* { dg-warning "TRUE" } */ +} + +/* Exactly one C derives from the shared S, and it is private in the most + derived object: /9.1 succeeds where /9.2 cannot. */ +struct OneC : private C { }; + +void test_virtual_src_one_enclosing_dst () { + OneC obj; + S *s = (S *) &obj; + __analyzer_eval (dynamic_cast<C *> (s) != NULL); /* { dg-warning "TRUE" } */ +} + +/* As above, but the operand was formed through a sibling that is not a + C, so the enclosing C is off that path. */ +struct NotC : virtual S { }; +struct SideC : NotC, private C { }; + +void test_virtual_src_dst_off_the_path () { + SideC obj; + S *s = (S *) (NotC *) &obj; + __analyzer_eval (dynamic_cast<C *> (s) != NULL); /* { dg-warning "TRUE" } */ +} diff --git a/gcc/testsuite/g++.dg/analyzer/dyncast-5.C b/gcc/testsuite/g++.dg/analyzer/dyncast-5.C new file mode 100644 index 00000000000..2bdcfde0a3c --- /dev/null +++ b/gcc/testsuite/g++.dg/analyzer/dyncast-5.C @@ -0,0 +1,30 @@ +/* /10: a failed cast to reference type throws std::bad_cast. */ + +#include <typeinfo> +#include "../../gcc.dg/analyzer/analyzer-decls.h" + +struct A { virtual ~A () {} }; +struct B : A { int m; }; + +void test_ref_success () { + B obj; + obj.m = 3; + A &a = obj; + B &b = dynamic_cast<B &> (a); + __analyzer_eval (b.m == 3); /* { dg-warning "TRUE" } */ +} + +void test_ref_failure () { + A obj; + A &a = obj; + try + { + B &b = dynamic_cast<B &> (a); + __analyzer_dump_path (); /* { dg-bogus "path" } */ + (void) b; + } + catch (std::bad_cast &) + { + __analyzer_dump_path (); /* { dg-message "path" } */ + } +} -- 2.54.0