Re: [PATCH v2 2/2] analyzer: add dynamic_cast support [PR110578]
David Malcolm <[email protected]>
| Newsgroups | gmane.comp.gcc.patches |
|---|---|
| Message-ID | <[email protected]> |
On Sun, 2026-08-16 at 20:24 +0100, Egas Ribeiro wrote:
Hi Egas
Thanks for the updated patch.
I spent some time today stepping through the code to get a sense for
how it works, and I think I understand it now, and the places where
you've been able to (re)use helper subroutines from ipa-devirt.cc
This patch is OK for trunk. Please push, assuming you've done your
usual testing.
One issue I noticed: I think we need a kf_cxa_bad_cast to handle
__cxa_bad_cast. Right now, for dyncast-5.C the patch emits:
../../src/gcc/testsuite/g++.dg/analyzer/dyncast-5.C: In function ‘void
test_ref_success()’:
../../src/gcc/testsuite/g++.dg/analyzer/dyncast-5.C:14:19: warning: TRUE
14 | __analyzer_eval (b.m == 3); /* { dg-warning "TRUE" } */
| ~~~~~~~~~~~~~~~~^~~~~~~~~~
../../src/gcc/testsuite/g++.dg/analyzer/dyncast-5.C: In function ‘void test_ref_failure()’:
../../src/gcc/testsuite/g++.dg/analyzer/dyncast-5.C:28:28: note: path
28 | __analyzer_dump_path (); /* { dg-message "path" } */
| ~~~~~~~~~~~~~~~~~~~~~^~
‘void test_ref_failure()’: event 1
22 | B &b = dynamic_cast<B &> (a);
| ^
| |
| (1) following ‘false’ branch... ─>─┐
| │
‘void test_ref_failure()’: events 2-5
| │
|┌────────────────────────────────────────────────────────────────────┘
22 |│ B &b = dynamic_cast<B &> (a);
|│ ^
|│ |
|└────────────────────────────────>(2) ...to here
| (3) if ‘void __cxa_bad_cast()’ throws an exception...
......
26 | catch (std::bad_cast &)
| ~
| |
| (4) ...catching exception here
27 | {
28 | __analyzer_dump_path (); /* { dg-message "path" } */
| ~~~~~~~~~~~~~~~~~~~~~~~
| |
| (5) ⚠️ here
Note how in the failure path, it looks like we're considering
__cxa_bad_cast as an unknown function that could throw an exception,
whereas we probably want to treat it as a known function that always
throws a bad_cast exception, as per
https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html#cxx-aux
But this can be done as followup work on top of these patches.
Thanks again; this is great work.
Dave
> 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:
>
> * 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".
> * 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/kf-lang-cp.cc | 293
> ++++++++++++++++++++++
> gcc/analyzer/region-model.cc | 69 +++--
> 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 +++
> 8 files changed, 627 insertions(+), 37 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/kf-lang-cp.cc b/gcc/analyzer/kf-lang-cp.cc
> index 5d57bcc5fcb..8b1a8bde720 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,292 @@ namespace ana {
>
> /* Implementations of specific functions. */
>
> +/* Handler for __dynamic_cast. */
> +
> +/* A candidate TYPE subobject found on one inheritance path. */
> +
> +struct dyncast_subobject
> +{
> + dyncast_subobject () : binfo (NULL_TREE), accessible (false) {}
> + dyncast_subobject (tree binfo, bool accessible)
> + : binfo (binfo), accessible (accessible)
> + {}
> +
> + 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;
> +
> + /* Start by assuming the path will be public. */
> + auto_vec<dyncast_subobject> dst_matches;
> + dyncast_subobject md_subobject = {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 +659,11 @@ 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> ());
> +
> + /* Itanium C++ ABI's "The dynamic_cast Algorithm"
> +
> https://itanium-cxx-abi.github.io/cxx-abi/abi.html#dynamic_cast-algorithm
> + */
> + kfm.add ("__dynamic_cast", std::make_unique<kf_dynamic_cast> ());
> }
>
> } // namespace ana
> diff --git a/gcc/analyzer/region-model.cc b/gcc/analyzer/region-
> model.cc
> index f385e008fa7..2666c2cd651 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,53 @@ 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)
> + *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" } */
> + }
> +}