Re: [PATCH] analyzer: add dynamic_cast support [PR110578]
David Malcolm <[email protected]>
| Newsgroups | gmane.comp.gcc.patches |
|---|---|
| Message-ID | <[email protected]> |
On Sat, 2026-08-01 at 13:14 +0100, Egas Ribeiro wrote: > 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. Sorry about the delay in reviewing this. Could you please split this patch up into two parts: (a) an initial part that just has these bits: 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. * known-function-manager.cc (known_function_manager::get_match): Also match functions declared in namespace __cxxabiv1. (b) everything else I think (a) is pretty-much ready to go. I'm attempting to review (b), trying to compare it to cxx_eval_dynamic_cast_fn, but I'm not very familiar with C++'s constexpr code, sorry. I'll continue to look at (b). Dave > > 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" } */ > + } > +}