[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