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" } */
> +    }
> +}

Reply via email to