diff mbox series

[pushed] analyzer: improvements to out-of-bounds diagrams [PR111155]

Message ID 20231008225854.783706-1-dmalcolm@redhat.com
State New
Headers show
Series [pushed] analyzer: improvements to out-of-bounds diagrams [PR111155] | expand

Commit Message

David Malcolm Oct. 8, 2023, 10:58 p.m. UTC
Update out-of-bounds diagrams to show existing string values,
and the initial write index within a string buffer.

For example, given the out-of-bounds write in strcat in:

void test (void)
{
  char buf[10];
  strcpy (buf, "hello");
  strcat (buf, " world!");
}

the diagram improves from:

                           ┌─────┬─────┬────┬────┬────┐┌─────┬─────┬─────┐
                           │ [0] │ [1] │[2] │[3] │[4] ││ [5] │ [6] │ [7] │
                           ├─────┼─────┼────┼────┼────┤├─────┼─────┼─────┤
                           │ ' ' │ 'w' │'o' │'r' │'l' ││ 'd' │ '!' │ NUL │
                           ├─────┴─────┴────┴────┴────┴┴─────┴─────┴─────┤
                           │      string literal (type: 'char[8]')       │
                           └─────────────────────────────────────────────┘
                              │     │    │    │    │      │     │     │
                              │     │    │    │    │      │     │     │
                              v     v    v    v    v      v     v     v
  ┌─────┬────────────────────────────────────────┬────┐┌─────────────────┐
  │ [0] │                  ...                   │[9] ││                 │
  ├─────┴────────────────────────────────────────┴────┤│after valid range│
  │             'buf' (type: 'char[10]')              ││                 │
  └───────────────────────────────────────────────────┘└─────────────────┘
  ├─────────────────────────┬─────────────────────────┤├────────┬────────┤
                            │                                   │
                  ╭─────────┴────────╮                ╭─────────┴─────────╮
                  │capacity: 10 bytes│                │overflow of 3 bytes│
                  ╰──────────────────╯                ╰───────────────────╯

to:

                             ┌────┬────┬────┬────┬────┐┌─────┬─────┬─────┐
                             │[0] │[1] │[2] │[3] │[4] ││ [5] │ [6] │ [7] │
                             ├────┼────┼────┼────┼────┤├─────┼─────┼─────┤
                             │' ' │'w' │'o' │'r' │'l' ││ 'd' │ '!' │ NUL │
                             ├────┴────┴────┴────┴────┴┴─────┴─────┴─────┤
                             │     string literal (type: 'char[8]')      │
                             └───────────────────────────────────────────┘
                               │    │    │    │    │      │     │     │
                               │    │    │    │    │      │     │     │
                               v    v    v    v    v      v     v     v
  ┌─────┬────────────────────┬────┬──────────────┬────┐┌─────────────────┐
  │ [0] │        ...         │[5] │     ...      │[9] ││                 │
  ├─────┼────┬────┬────┬────┬┼────┼──────────────┴────┘│                 │
  │ 'h' │'e' │'l' │'l' │'o' ││NUL │                    │after valid range│
  ├─────┴────┴────┴────┴────┴┴────┴───────────────────┐│                 │
  │             'buf' (type: 'char[10]')              ││                 │
  └───────────────────────────────────────────────────┘└─────────────────┘
  ├─────────────────────────┬─────────────────────────┤├────────┬────────┤
                            │                                   │
                  ╭─────────┴────────╮                ╭─────────┴─────────╮
                  │capacity: 10 bytes│                │overflow of 3 bytes│
                  ╰──────────────────╯                ╰───────────────────╯

Successfully bootstrapped & regrtested on x86_64-pc-linux-gnu.
Pushed to trunk as r14-4477-gb365e9d57ad445.

gcc/analyzer/ChangeLog:
	PR analyzer/111155
	* access-diagram.cc (boundaries::boundaries): Add logger param
	(boundaries::add): Add logging.
	(boundaries::get_hard_boundaries_in_range): New.
	(boundaries::m_logger): New field.
	(boundaries::get_table_x_for_offset): Make public.
	(class svalue_spatial_item): New.
	(class compound_svalue_spatial_item): New.
	(add_ellipsis_to_gaps): New.
	(valid_region_spatial_item::valid_region_spatial_item): Add theme
	param.  Initialize m_boundaries, m_existing_sval, and
	m_existing_sval_spatial_item.
	(valid_region_spatial_item::add_boundaries): Set m_boundaries.
	Add boundaries for any m_existing_sval_spatial_item.
	(valid_region_spatial_item::add_array_elements_to_table): Rewrite
	creation of min/max index in terms of
	maybe_add_array_index_to_table.  Rewrite ellipsis code using
	add_ellipsis_to_gaps. Add index values for any hard boundaries
	within the valid region.
	(valid_region_spatial_item::maybe_add_array_index_to_table): New,
	based on code formerly in add_array_elements_to_table.
	(valid_region_spatial_item::make_table): Make use of
	m_existing_sval_spatial_item, if any.
	(valid_region_spatial_item::m_boundaries): New field.
	(valid_region_spatial_item::m_existing_sval): New field.
	(valid_region_spatial_item::m_existing_sval_spatial_item): New
	field.
	(class svalue_spatial_item): Rename to...
	(class written_svalue_spatial_item): ...this.
	(class string_region_spatial_item): Rename to..
	(class string_literal_spatial_item): ...this.  Add "kind".
	(string_literal_spatial_item::add_boundaries): Use m_kind to
	determine kind of boundary.  Update for renaming of m_actual_bits
	to m_bits.
	(string_literal_spatial_item::make_table): Likewise.  Support not
	displaying a row for byte indexes, and not displaying a row for
	the type.
	(string_literal_spatial_item::add_column_for_byte): Make byte index
	row optional.
	(svalue_spatial_item::make): Convert to...
	(make_written_svalue_spatial_item): ...this.
	(make_existing_svalue_spatial_item): New.
	(access_diagram_impl::access_diagram_impl): Pass theme to
	m_valid_region_spatial_item ctor.  Update for renaming of
	m_svalue_spatial_item.
	(access_diagram_impl::find_boundaries): Pass logger to boundaries.
	Update for renaming of...
	(access_diagram_impl::m_svalue_spatial_item): Rename to...
	(access_diagram_impl::m_written_svalue_spatial_item): ...this.

gcc/testsuite/ChangeLog:
	PR analyzer/111155
	* c-c++-common/analyzer/out-of-bounds-diagram-strcat-2.c: New test.
	* c-c++-common/analyzer/out-of-bounds-diagram-strcat.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-17.c: Update expected
	result to show the existing content of "buf" and the index at
	which the write starts.
	* gcc.dg/analyzer/out-of-bounds-diagram-18.c: Likewise.
	* gcc.dg/analyzer/out-of-bounds-diagram-19.c: Likewise.
	* gcc.dg/analyzer/out-of-bounds-diagram-6.c: Update expected
	output.

gcc/ChangeLog:
	PR analyzer/111155
	* text-art/table.cc (table::maybe_set_cell_span): New.
	(table::add_other_table): New.
	* text-art/table.h (class table::cell_placement): Add class table
	as a friend.
	(table::add_rows): New.
	(table::add_row): Reimplement in terms of add_rows.
	(table::maybe_set_cell_span): New decl.
	(table::add_other_table): New decl.
	* text-art/types.h (operator+): New operator for rect + coord.
---
 gcc/analyzer/access-diagram.cc                | 430 ++++++++++++++----
 .../analyzer/out-of-bounds-diagram-strcat-2.c |  74 +++
 .../analyzer/out-of-bounds-diagram-strcat.c   |  66 +++
 .../analyzer/out-of-bounds-diagram-17.c       |  28 +-
 .../analyzer/out-of-bounds-diagram-18.c       |  54 ++-
 .../analyzer/out-of-bounds-diagram-19.c       |  42 +-
 .../gcc.dg/analyzer/out-of-bounds-diagram-6.c |  68 +--
 gcc/text-art/table.cc                         |  35 ++
 gcc/text-art/table.h                          |  21 +-
 gcc/text-art/types.h                          |   7 +
 10 files changed, 644 insertions(+), 181 deletions(-)
 create mode 100644 gcc/testsuite/c-c++-common/analyzer/out-of-bounds-diagram-strcat-2.c
 create mode 100644 gcc/testsuite/c-c++-common/analyzer/out-of-bounds-diagram-strcat.c

Comments

Tobias Burnus Oct. 9, 2023, 10:09 a.m. UTC | #1
Hi David,

your commit breaks compilation with GCC < 6, here with GCC 5.2:

gcc/analyzer/access-diagram.cc: In member function 'void ana::boundaries::add(const ana::access_range&, ana::boundaries::kind)':
gcc/analyzer/access-diagram.cc:655:20: error: 'kind' is not a class, namespace, or enumeration
            (kind == kind::HARD) ? "HARD" : "soft");
                     ^
The problem is ...

On 09.10.23 00:58, David Malcolm wrote:

> Update out-of-bounds diagrams to show existing string values,
> diff --git a/gcc/analyzer/access-diagram.cc b/gcc/analyzer/access-diagram.cc
> index a51d594b5b2..2197ec63f53 100644
> --- a/gcc/analyzer/access-diagram.cc
> +++ b/gcc/analyzer/access-diagram.cc
> @@ -630,8 +630,8 @@ class boundaries
>   public:
>     enum class kind { HARD, SOFT};

...

> @@ -646,6 +646,15 @@ public:

Just above the following diff is the line:

   void add (const access_range &range, enum kind kind)

>     {
>       add (range.m_start, kind);
>       add (range.m_next, kind);
> +    if (m_logger)
> +      {
> +     m_logger->start_log_line ();
> +     m_logger->log_partial ("added access_range: ");
> +     range.dump_to_pp (m_logger->get_printer (), true);
> +     m_logger->log_partial (" (%s)",
> +                            (kind == kind::HARD) ? "HARD" : "soft");
> +     m_logger->end_log_line ();

Actual problem:

Playing around also with the compiler explorer shows that GCC 5.2 or likewise 5.5
do not like the variable (PARAM_DECL) name "kind" combined with  "kind::HARD".

The following works:
(A) Using "kind == boundaries::kind::HARD" - i.e. adding "boundaries::"
(B) Renaming the parameter name "kind" to something else - like "k" as used
     in the other functions.

Can you fix it?

Thanks,

Tobias

-----------------
Siemens Electronic Design Automation GmbH; Anschrift: Arnulfstraße 201, 80634 München; Gesellschaft mit beschränkter Haftung; Geschäftsführer: Thomas Heurung, Frank Thürauf; Sitz der Gesellschaft: München; Registergericht München, HRB 106955
David Malcolm Oct. 9, 2023, 2:08 p.m. UTC | #2
On Mon, 2023-10-09 at 12:09 +0200, Tobias Burnus wrote:
> Hi David,
> 
> your commit breaks compilation with GCC < 6, here with GCC 5.2:
> 
> gcc/analyzer/access-diagram.cc: In member function 'void
> ana::boundaries::add(const ana::access_range&,
> ana::boundaries::kind)':
> gcc/analyzer/access-diagram.cc:655:20: error: 'kind' is not a class,
> namespace, or enumeration
>             (kind == kind::HARD) ? "HARD" : "soft");
>                      ^
> The problem is ...
> 
> On 09.10.23 00:58, David Malcolm wrote:
> 
> > Update out-of-bounds diagrams to show existing string values,
> > diff --git a/gcc/analyzer/access-diagram.cc b/gcc/analyzer/access-
> > diagram.cc
> > index a51d594b5b2..2197ec63f53 100644
> > --- a/gcc/analyzer/access-diagram.cc
> > +++ b/gcc/analyzer/access-diagram.cc
> > @@ -630,8 +630,8 @@ class boundaries
> >   public:
> >     enum class kind { HARD, SOFT};
> 
> ...
> 
> > @@ -646,6 +646,15 @@ public:
> 
> Just above the following diff is the line:
> 
>    void add (const access_range &range, enum kind kind)
> 
> >     {
> >       add (range.m_start, kind);
> >       add (range.m_next, kind);
> > +    if (m_logger)
> > +      {
> > +     m_logger->start_log_line ();
> > +     m_logger->log_partial ("added access_range: ");
> > +     range.dump_to_pp (m_logger->get_printer (), true);
> > +     m_logger->log_partial (" (%s)",
> > +                            (kind == kind::HARD) ? "HARD" :
> > "soft");
> > +     m_logger->end_log_line ();
> 
> Actual problem:
> 
> Playing around also with the compiler explorer shows that GCC 5.2 or
> likewise 5.5
> do not like the variable (PARAM_DECL) name "kind" combined with 
> "kind::HARD".
> 
> The following works:
> (A) Using "kind == boundaries::kind::HARD" - i.e. adding
> "boundaries::"
> (B) Renaming the parameter name "kind" to something else - like "k"
> as used
>      in the other functions.
> 
> Can you fix it?

Sorry about the breakage, and thanks for the investigation.

Does the following patch fix the build for you?
Thanks


gcc/analyzer/ChangeLog:
	* access-diagram.cc (boundaries::add): Explicitly state
	"boundaries::" scope for "kind" enum.
---
 gcc/analyzer/access-diagram.cc | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/gcc/analyzer/access-diagram.cc b/gcc/analyzer/access-diagram.cc
index 2197ec63f53..c7d190e3188 100644
--- a/gcc/analyzer/access-diagram.cc
+++ b/gcc/analyzer/access-diagram.cc
@@ -652,7 +652,8 @@ public:
 	m_logger->log_partial ("added access_range: ");
 	range.dump_to_pp (m_logger->get_printer (), true);
 	m_logger->log_partial (" (%s)",
-			       (kind == kind::HARD) ? "HARD" : "soft");
+			       (kind == boundaries::kind::HARD)
+			       ? "HARD" : "soft");
 	m_logger->end_log_line ();
       }
   }
Tobias Burnus Oct. 9, 2023, 3:01 p.m. UTC | #3
Hi David,

On 09.10.23 16:08, David Malcolm wrote:
> On Mon, 2023-10-09 at 12:09 +0200, Tobias Burnus wrote:
>> The following works:
>> (A) Using "kind == boundaries::kind::HARD" - i.e. adding
>> "boundaries::"
>> (B) Renaming the parameter name "kind" to something else - like "k"
>> as used
>>       in the other functions.
>>
>> Can you fix it?
> Sorry about the breakage, and thanks for the investigation.
Well, without an older compiler, one does not see it. It also worked
flawlessly on my laptop today.
> Does the following patch fix the build for you?

Yes – as mentioned either of the variants above should work and (A) is
what you have in your patch.

And it is what I actually tried for the full build. Hence, yes, it works :-)

Thanks for the quick action!

Tobias

> gcc/analyzer/ChangeLog:
>       * access-diagram.cc (boundaries::add): Explicitly state
>       "boundaries::" scope for "kind" enum.
> ---
>   gcc/analyzer/access-diagram.cc | 3 ++-
>   1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git a/gcc/analyzer/access-diagram.cc b/gcc/analyzer/access-diagram.cc
> index 2197ec63f53..c7d190e3188 100644
> --- a/gcc/analyzer/access-diagram.cc
> +++ b/gcc/analyzer/access-diagram.cc
> @@ -652,7 +652,8 @@ public:
>       m_logger->log_partial ("added access_range: ");
>       range.dump_to_pp (m_logger->get_printer (), true);
>       m_logger->log_partial (" (%s)",
> -                            (kind == kind::HARD) ? "HARD" : "soft");
> +                            (kind == boundaries::kind::HARD)
> +                            ? "HARD" : "soft");
>       m_logger->end_log_line ();
>         }
>     }
-----------------
Siemens Electronic Design Automation GmbH; Anschrift: Arnulfstraße 201, 80634 München; Gesellschaft mit beschränkter Haftung; Geschäftsführer: Thomas Heurung, Frank Thürauf; Sitz der Gesellschaft: München; Registergericht München, HRB 106955
David Malcolm Oct. 9, 2023, 7:14 p.m. UTC | #4
On Mon, 2023-10-09 at 17:01 +0200, Tobias Burnus wrote:
> Hi David,
> 
> On 09.10.23 16:08, David Malcolm wrote:
> > On Mon, 2023-10-09 at 12:09 +0200, Tobias Burnus wrote:
> > > The following works:
> > > (A) Using "kind == boundaries::kind::HARD" - i.e. adding
> > > "boundaries::"
> > > (B) Renaming the parameter name "kind" to something else - like
> > > "k"
> > > as used
> > >       in the other functions.
> > > 
> > > Can you fix it?
> > Sorry about the breakage, and thanks for the investigation.
> Well, without an older compiler, one does not see it. It also worked
> flawlessly on my laptop today.
> > Does the following patch fix the build for you?
> 
> Yes – as mentioned either of the variants above should work and (A)
> is
> what you have in your patch.
> 
> And it is what I actually tried for the full build. Hence, yes, it
> works :-)

Thanks!

I've pushed this to trunk as r14-4521-g08d0f840dc7ad2.
diff mbox series

Patch

diff --git a/gcc/analyzer/access-diagram.cc b/gcc/analyzer/access-diagram.cc
index a51d594b5b2..2197ec63f53 100644
--- a/gcc/analyzer/access-diagram.cc
+++ b/gcc/analyzer/access-diagram.cc
@@ -630,8 +630,8 @@  class boundaries
 public:
   enum class kind { HARD, SOFT};
 
-  boundaries (const region &base_reg)
-  : m_base_reg (base_reg)
+  boundaries (const region &base_reg, logger *logger)
+  : m_base_reg (base_reg), m_logger (logger)
   {
   }
 
@@ -646,6 +646,15 @@  public:
   {
     add (range.m_start, kind);
     add (range.m_next, kind);
+    if (m_logger)
+      {
+	m_logger->start_log_line ();
+	m_logger->log_partial ("added access_range: ");
+	range.dump_to_pp (m_logger->get_printer (), true);
+	m_logger->log_partial (" (%s)",
+			       (kind == kind::HARD) ? "HARD" : "soft");
+	m_logger->end_log_line ();
+      }
   }
 
   void add (const region &reg, region_model_manager *mgr, enum kind kind)
@@ -714,8 +723,30 @@  public:
     return m_all_offsets.size ();
   }
 
+  std::vector<region_offset>
+  get_hard_boundaries_in_range (byte_offset_t min_offset,
+				byte_offset_t max_offset) const
+  {
+    std::vector<region_offset> result;
+    for (auto &offset : m_hard_offsets)
+      {
+	if (!offset.concrete_p ())
+	  continue;
+	byte_offset_t byte;
+	if (!offset.get_concrete_byte_offset (&byte))
+	  continue;
+	if (byte < min_offset)
+	  continue;
+	if (byte > max_offset)
+	  continue;
+	result.push_back (offset);
+      }
+    return result;
+  }
+
 private:
   const region &m_base_reg;
+  logger *m_logger;
   std::set<region_offset> m_all_offsets;
   std::set<region_offset> m_hard_offsets;
 };
@@ -1085,7 +1116,6 @@  public:
     logger.dec_indent ();
   }
 
-private:
   int get_table_x_for_offset (region_offset offset) const
   {
     auto slot = m_table_x_for_offset.find (offset);
@@ -1097,6 +1127,7 @@  private:
     return slot->second;
   }
 
+private:
   int get_table_x_for_prev_offset (region_offset offset) const
   {
     auto slot = m_table_x_for_prev_offset.find (offset);
@@ -1132,6 +1163,124 @@  public:
 			    style_manager &sm) const = 0;
 };
 
+/* A spatial_item that involves showing an svalue at a particular offset.  */
+
+class svalue_spatial_item : public spatial_item
+{
+public:
+  enum class kind
+  {
+     WRITTEN,
+     EXISTING
+  };
+protected:
+  svalue_spatial_item (const svalue &sval,
+		       access_range bits,
+		       enum kind kind)
+  : m_sval (sval), m_bits (bits), m_kind (kind)
+  {
+  }
+
+  const svalue &m_sval;
+  access_range m_bits;
+  enum kind m_kind;
+};
+
+static std::unique_ptr<spatial_item>
+make_existing_svalue_spatial_item (const svalue *sval,
+				   const access_range &bits,
+				   const theme &theme);
+
+class compound_svalue_spatial_item : public svalue_spatial_item
+{
+public:
+  compound_svalue_spatial_item (const compound_svalue &sval,
+				const access_range &bits,
+				enum kind kind,
+				const theme &theme)
+  : svalue_spatial_item (sval, bits, kind),
+    m_compound_sval (sval)
+  {
+    const binding_map &map = m_compound_sval.get_map ();
+    auto_vec <const binding_key *> binding_keys;
+    for (auto iter : map)
+      {
+	const binding_key *key = iter.first;
+	const svalue *bound_sval = iter.second;
+	if (const concrete_binding *concrete_key
+	      = key->dyn_cast_concrete_binding ())
+	  {
+	    access_range range (nullptr,
+				concrete_key->get_bit_range ());
+	    if (std::unique_ptr<spatial_item> child
+		  = make_existing_svalue_spatial_item (bound_sval,
+						       range,
+						       theme))
+	      m_children.push_back (std::move (child));
+	  }
+      }
+  }
+
+  void add_boundaries (boundaries &out, logger *logger) const final override
+  {
+    LOG_SCOPE (logger);
+    for (auto &iter : m_children)
+      iter->add_boundaries (out, logger);
+  }
+
+  table make_table (const bit_to_table_map &btm,
+		    style_manager &sm) const final override
+  {
+    std::vector<table> child_tables;
+    int max_rows = 0;
+    for (auto &iter : m_children)
+      {
+	table child_table (iter->make_table (btm, sm));
+	max_rows = MAX (max_rows, child_table.get_size ().h);
+	child_tables.push_back (std::move (child_table));
+      }
+    table t (table::size_t (btm.get_num_columns (), max_rows));
+    for (auto &&child_table : child_tables)
+      t.add_other_table (std::move (child_table),
+			 table::coord_t (0, 0));
+    return t;
+  }
+
+private:
+  const compound_svalue &m_compound_sval;
+  std::vector<std::unique_ptr<spatial_item>> m_children;
+};
+
+/* Loop through the TABLE_X_RANGE columns of T, adding
+   cells containing "..." in any unoccupied ranges of table cell.  */
+
+static void
+add_ellipsis_to_gaps (table &t,
+		      style_manager &sm,
+		      const table::range_t &table_x_range,
+		      const table::range_t &table_y_range)
+{
+  int table_x = table_x_range.get_min ();
+  while (table_x < table_x_range.get_next ())
+    {
+      /* Find a run of unoccupied table cells.  */
+      const int start_table_x = table_x;
+      while (table_x < table_x_range.get_next ()
+	     && !t.get_placement_at (table::coord_t (table_x,
+						     table_y_range.get_min ())))
+	table_x++;
+      const table::range_t unoccupied_x_range (start_table_x, table_x);
+      if (unoccupied_x_range.get_size () > 0)
+	t.set_cell_span (table::rect_t (unoccupied_x_range, table_y_range),
+			 styled_string (sm, "..."));
+      /* Skip occupied table cells.  */
+      while (table_x < table_x_range.get_next ()
+	     && t.get_placement_at (table::coord_t (table_x,
+						    table_y_range.get_min ())))
+	table_x++;
+    }
+}
+
 /* Subclass of spatial_item for visualizing the region of memory
    that's valid to access relative to the base region of region accessed in
    the operation.  */
@@ -1140,14 +1289,23 @@  class valid_region_spatial_item : public spatial_item
 {
 public:
   valid_region_spatial_item (const access_operation &op,
-			     diagnostic_event_id_t region_creation_event_id)
+			     diagnostic_event_id_t region_creation_event_id,
+			     const theme &theme)
   : m_op (op),
-    m_region_creation_event_id (region_creation_event_id)
-  {}
+    m_region_creation_event_id (region_creation_event_id),
+    m_boundaries (nullptr),
+    m_existing_sval (op.m_model.get_store_value (op.m_base_region, nullptr)),
+    m_existing_sval_spatial_item
+      (make_existing_svalue_spatial_item (m_existing_sval,
+					  op.get_valid_bits (),
+					  theme))
+  {
+  }
 
   void add_boundaries (boundaries &out, logger *logger) const final override
   {
     LOG_SCOPE (logger);
+    m_boundaries = &out;
     access_range valid_bits = m_op.get_valid_bits ();
     if (logger)
       {
@@ -1158,6 +1316,18 @@  public:
       }
     out.add (valid_bits, boundaries::kind::HARD);
 
+    if (m_existing_sval_spatial_item)
+      {
+	if (logger)
+	  {
+	    logger->start_log_line ();
+	    logger->log_partial ("existing svalue: ");
+	    m_existing_sval->dump_to_pp (logger->get_printer (), true);
+	    logger->end_log_line ();
+	  }
+	m_existing_sval_spatial_item->add_boundaries (out, logger);
+      }
+
     /* Support for showing first and final element in array types.  */
     if (tree base_type = m_op.m_base_region->get_type ())
       if (TREE_CODE (base_type) == ARRAY_TYPE)
@@ -1193,65 +1363,102 @@  public:
   {
     tree base_type = m_op.m_base_region->get_type ();
     gcc_assert (TREE_CODE (base_type) == ARRAY_TYPE);
+    gcc_assert (m_boundaries != nullptr);
 
     tree domain = TYPE_DOMAIN (base_type);
     if (!(TYPE_MIN_VALUE (domain) && TYPE_MAX_VALUE (domain)))
       return;
 
-    region_model_manager * const mgr = m_op.get_manager ();
     const int table_y = 0;
     const int table_h = 1;
     const table::range_t table_y_range (table_y, table_y + table_h);
 
     t.add_row ();
-    const svalue *min_idx_sval
-      = mgr->get_or_create_constant_svalue (TYPE_MIN_VALUE (domain));
-    const region *min_element = mgr->get_element_region (m_op.m_base_region,
-							 TREE_TYPE (base_type),
-							 min_idx_sval);
-    const access_range min_element_range (*min_element, mgr);
-    const table::range_t min_element_x_range
-      = btm.get_table_x_for_range (min_element_range);
-
-    t.set_cell_span (table::rect_t (min_element_x_range,
-				    table_y_range),
-		     fmt_styled_string (sm, "[%E]",
-					TYPE_MIN_VALUE (domain)));
-
-    const svalue *max_idx_sval
-      = mgr->get_or_create_constant_svalue (TYPE_MAX_VALUE (domain));
-    const region *max_element = mgr->get_element_region (m_op.m_base_region,
+
+    const table::range_t min_x_range
+      = maybe_add_array_index_to_table (t, btm, sm, table_y_range,
+					TYPE_MIN_VALUE (domain));
+    const table::range_t max_x_range
+      = maybe_add_array_index_to_table (t, btm, sm, table_y_range,
+					TYPE_MAX_VALUE (domain));
+
+    if (TREE_TYPE (base_type) == char_type_node)
+      {
+	/* For a char array,: if there are any hard boundaries in
+	   m_boundaries that are *within* the valid region,
+	   then show those index values.  */
+	std::vector<region_offset> hard_boundaries
+	  = m_boundaries->get_hard_boundaries_in_range
+	      (tree_to_shwi (TYPE_MIN_VALUE (domain)),
+	       tree_to_shwi (TYPE_MAX_VALUE (domain)));
+	for (auto &offset : hard_boundaries)
+	  {
+	    const int table_x = btm.get_table_x_for_offset (offset);
+	    if (!offset.concrete_p ())
+	      continue;
+	    byte_offset_t byte;
+	    if (!offset.get_concrete_byte_offset (&byte))
+	      continue;
+	    table::range_t table_x_range (table_x, table_x + 1);
+	    t.maybe_set_cell_span (table::rect_t (table_x_range,
+						  table_y_range),
+				   fmt_styled_string (sm, "[%wi]",
+						      byte.to_shwi ()));
+	  }
+      }
+
+    add_ellipsis_to_gaps (t, sm,
+			  table::range_t (min_x_range.get_next (),
+					  max_x_range.get_min ()),
+			  table_y_range);
+  }
+
+  table::range_t
+  maybe_add_array_index_to_table (table &t,
+				  const bit_to_table_map &btm,
+				  style_manager &sm,
+				  const table::range_t table_y_range,
+				  tree idx_cst) const
+  {
+    region_model_manager * const mgr = m_op.get_manager ();
+    tree base_type = m_op.m_base_region->get_type ();
+    const svalue *idx_sval
+      = mgr->get_or_create_constant_svalue (idx_cst);
+    const region *element_reg = mgr->get_element_region (m_op.m_base_region,
 							 TREE_TYPE (base_type),
-							 max_idx_sval);
-    if (min_element == max_element)
-      return; // 1-element array
+							 idx_sval);
+    const access_range element_range (*element_reg, mgr);
+    const table::range_t element_x_range
+      = btm.get_table_x_for_range (element_range);
 
-    const access_range max_element_range (*max_element, mgr);
-    const table::range_t max_element_x_range
-      = btm.get_table_x_for_range (max_element_range);
-    t.set_cell_span (table::rect_t (max_element_x_range,
-				    table_y_range),
-		     fmt_styled_string (sm, "[%E]",
-					TYPE_MAX_VALUE (domain)));
+    t.maybe_set_cell_span (table::rect_t (element_x_range,
+					  table_y_range),
+			   fmt_styled_string (sm, "[%E]", idx_cst));
 
-    const table::range_t other_elements_x_range (min_element_x_range.next,
-						 max_element_x_range.start);
-    if (other_elements_x_range.get_size () > 0)
-      t.set_cell_span (table::rect_t (other_elements_x_range, table_y_range),
-		       styled_string (sm, "..."));
+    return element_x_range;
   }
 
   table make_table (const bit_to_table_map &btm,
 		    style_manager &sm) const final override
   {
-    table t (table::size_t (btm.get_num_columns (), 1));
+    table t (table::size_t (btm.get_num_columns (), 0));
 
     if (tree base_type = m_op.m_base_region->get_type ())
       if (TREE_CODE (base_type) == ARRAY_TYPE)
 	add_array_elements_to_table (t, btm, sm);
 
+    /* Make use of m_existing_sval_spatial_item, if any.  */
+    if (m_existing_sval_spatial_item)
+      {
+	table table_for_existing
+	  = m_existing_sval_spatial_item->make_table (btm, sm);
+	const int table_y = t.add_rows (table_for_existing.get_size ().h);
+	t.add_other_table (std::move (table_for_existing),
+			   table::coord_t (0, table_y));
+      }
+
     access_range valid_bits = m_op.get_valid_bits ();
-    const int table_y = t.get_size ().h - 1;
+    const int table_y = t.add_row ();
     const int table_h = 1;
     table::rect_t rect = btm.get_table_rect (valid_bits, table_y, table_h);
     styled_string s;
@@ -1306,6 +1513,9 @@  public:
 private:
   const access_operation &m_op;
   diagnostic_event_id_t m_region_creation_event_id;
+  mutable const boundaries *m_boundaries;
+  const svalue *m_existing_sval;
+  std::unique_ptr<spatial_item> m_existing_sval_spatial_item;
 };
 
 /* Subclass of spatial_item for visualizing the region of memory
@@ -1362,15 +1572,10 @@  private:
    to the accessed region.
    Can be subclassed to give visualizations of specific kinds of svalue.  */
 
-class svalue_spatial_item : public spatial_item
+class written_svalue_spatial_item : public spatial_item
 {
 public:
-  static std::unique_ptr<svalue_spatial_item> make (const access_operation &op,
-						    const svalue &sval,
-						    access_range actual_bits,
-						    const theme &theme);
-
-  svalue_spatial_item (const access_operation &op,
+  written_svalue_spatial_item (const access_operation &op,
 		       const svalue &sval,
 		       access_range actual_bits)
   : m_op (op), m_sval (sval), m_actual_bits (actual_bits)
@@ -1479,15 +1684,15 @@  protected:
      └──────────────────────────────────────────────────────────────────────┘
 */
 
-class string_region_spatial_item : public svalue_spatial_item
+class string_literal_spatial_item : public svalue_spatial_item
 {
 public:
-  string_region_spatial_item (const access_operation &op,
-			      const svalue &sval,
-			      access_range actual_bits,
-			      const string_region &string_reg,
-			      const theme &theme)
-  : svalue_spatial_item (op, sval, actual_bits),
+  string_literal_spatial_item (const svalue &sval,
+			       access_range actual_bits,
+			       const string_region &string_reg,
+			       const theme &theme,
+			       enum kind kind)
+  : svalue_spatial_item (sval, actual_bits, kind),
     m_string_reg (string_reg),
     m_theme (theme),
     m_ellipsis_threshold (param_analyzer_text_art_string_ellipsis_threshold),
@@ -1501,16 +1706,18 @@  public:
   void add_boundaries (boundaries &out, logger *logger) const override
   {
     LOG_SCOPE (logger);
-    out.add (m_actual_bits, boundaries::kind::HARD);
+    out.add (m_bits, m_kind == svalue_spatial_item::kind::WRITTEN
+	     ? boundaries::kind::HARD
+	     : boundaries::kind::SOFT);
 
     tree string_cst = get_string_cst ();
     /* TREE_STRING_LENGTH is sizeof, not strlen.  */
     if (m_show_full_string)
-      out.add_all_bytes_in_range (m_actual_bits);
+      out.add_all_bytes_in_range (m_bits);
     else
       {
 	byte_range bytes (0, 0);
-	bool valid = m_actual_bits.as_concrete_byte_range (&bytes);
+	bool valid = m_bits.as_concrete_byte_range (&bytes);
 	gcc_assert (valid);
 	byte_range head_of_string (bytes.get_start_byte_offset (),
 				   m_ellipsis_head_len);
@@ -1532,11 +1739,13 @@  public:
   {
     table t (table::size_t (btm.get_num_columns (), 0));
 
-    const int byte_idx_table_y = t.add_row ();
+    const int byte_idx_table_y = (m_kind == svalue_spatial_item::kind::WRITTEN
+				  ? t.add_row ()
+				  : -1);
     const int byte_val_table_y = t.add_row ();
 
     byte_range bytes (0, 0);
-    bool valid = m_actual_bits.as_concrete_byte_range (&bytes);
+    bool valid = m_bits.as_concrete_byte_range (&bytes);
     gcc_assert (valid);
     tree string_cst = get_string_cst ();
     if (m_show_full_string)
@@ -1616,14 +1825,17 @@  public:
 			       byte_idx,
 			       byte_idx_table_y, byte_val_table_y);
 
-	/* Ellipsis (two rows high).  */
+	/* Ellipsis.  */
 	const byte_range ellipsis_bytes
 	  (m_ellipsis_head_len + bytes.get_start_byte_offset (),
 	   TREE_STRING_LENGTH (string_cst)
 	   - (m_ellipsis_head_len + m_ellipsis_tail_len));
 	const table::rect_t table_rect
-	  = btm.get_table_rect (&m_string_reg, ellipsis_bytes,
-				byte_idx_table_y, 2);
+	  = ((byte_idx_table_y != -1)
+	     ? btm.get_table_rect (&m_string_reg, ellipsis_bytes,
+				   byte_idx_table_y, 2)
+	     : btm.get_table_rect (&m_string_reg, ellipsis_bytes,
+				   byte_val_table_y, 1));
 	t.set_cell_span(table_rect, styled_string (sm, "..."));
 
 	/* Tail of string.  */
@@ -1637,12 +1849,15 @@  public:
 			       byte_idx_table_y, byte_val_table_y);
       }
 
-    const int summary_table_y = t.add_row ();
-    t.set_cell_span (btm.get_table_rect (&m_string_reg, bytes,
-					 summary_table_y, 1),
-		     fmt_styled_string (sm,
-					_("string literal (type: %qT)"),
-					TREE_TYPE (string_cst)));
+    if (m_kind == svalue_spatial_item::kind::WRITTEN)
+      {
+	const int summary_table_y = t.add_row ();
+	t.set_cell_span (btm.get_table_rect (&m_string_reg, bytes,
+					     summary_table_y, 1),
+			 fmt_styled_string (sm,
+					    _("string literal (type: %qT)"),
+					    TREE_TYPE (string_cst)));
+      }
 
     return t;
   }
@@ -1687,7 +1902,7 @@  private:
     gcc_assert (byte_idx_within_string < TREE_STRING_LENGTH (string_cst));
 
     const byte_range bytes (byte_idx_within_cluster, 1);
-    if (1) // show_byte_indices
+    if (byte_idx_table_y != -1)
       {
 	const table::rect_t idx_table_rect
 	  = btm.get_table_rect (&m_string_reg, bytes, byte_idx_table_y, 1);
@@ -1729,18 +1944,54 @@  private:
   const bool m_show_utf8;
 };
 
-std::unique_ptr<svalue_spatial_item>
-svalue_spatial_item::make (const access_operation &op,
-			   const svalue &sval,
-			   access_range actual_bits,
-			   const theme &theme)
+static std::unique_ptr<spatial_item>
+make_written_svalue_spatial_item (const access_operation &op,
+				  const svalue &sval,
+				  access_range actual_bits,
+				  const theme &theme)
 {
   if (const initial_svalue *initial_sval = sval.dyn_cast_initial_svalue ())
     if (const string_region *string_reg
 	= initial_sval->get_region ()->dyn_cast_string_region ())
-      return make_unique <string_region_spatial_item> (op, sval, actual_bits,
-						       *string_reg, theme);
-  return make_unique <svalue_spatial_item> (op, sval, actual_bits);
+      return make_unique <string_literal_spatial_item>
+	(sval, actual_bits,
+	 *string_reg, theme,
+	 svalue_spatial_item::kind::WRITTEN);
+  return make_unique <written_svalue_spatial_item> (op, sval, actual_bits);
+}
+
+static std::unique_ptr<spatial_item>
+make_existing_svalue_spatial_item (const svalue *sval,
+				   const access_range &bits,
+				   const theme &theme)
+{
+  if (!sval)
+    return nullptr;
+
+  switch (sval->get_kind ())
+    {
+    default:
+      return nullptr;
+
+    case SK_INITIAL:
+      {
+	const initial_svalue *initial_sval = (const initial_svalue *)sval;
+	if (const string_region *string_reg
+	    = initial_sval->get_region ()->dyn_cast_string_region ())
+	  return make_unique <string_literal_spatial_item>
+	    (*sval, bits,
+	     *string_reg, theme,
+	     svalue_spatial_item::kind::EXISTING);
+	return nullptr;
+      }
+
+    case SK_COMPOUND:
+      return make_unique<compound_svalue_spatial_item>
+	(*((const compound_svalue *)sval),
+	 bits,
+	 svalue_spatial_item::kind::EXISTING,
+	 theme);
+    }
 }
 
 /* Widget subclass implementing access diagrams.  */
@@ -1759,7 +2010,7 @@  public:
     m_theme (theme),
     m_logger (logger),
     m_invalid (false),
-    m_valid_region_spatial_item (op, region_creation_event_id),
+    m_valid_region_spatial_item (op, region_creation_event_id, theme),
     m_accessed_region_spatial_item (op),
     m_btm (),
     m_calc_req_size_called (false)
@@ -1800,10 +2051,11 @@  public:
     if (op.m_sval_hint)
       {
 	access_range actual_bits = m_op.get_actual_bits ();
-	m_svalue_spatial_item = svalue_spatial_item::make (m_op,
-							   *op.m_sval_hint,
-							   actual_bits,
-							   m_theme);
+	m_written_svalue_spatial_item
+	  = make_written_svalue_spatial_item (m_op,
+					      *op.m_sval_hint,
+					      actual_bits,
+					      m_theme);
       }
 
     /* Two passes:
@@ -1856,9 +2108,9 @@  public:
 	add_aligned_child_table (std::move (t_headings));
       }
 
-    if (m_svalue_spatial_item)
+    if (m_written_svalue_spatial_item)
       {
-	table t_sval (m_svalue_spatial_item->make_table (m_btm, m_sm));
+	table t_sval (m_written_svalue_spatial_item->make_table (m_btm, m_sm));
 	add_aligned_child_table (std::move (t_sval));
       }
     else
@@ -1942,12 +2194,12 @@  private:
   find_boundaries () const
   {
     std::unique_ptr<boundaries> result
-      = make_unique<boundaries> (*m_op.m_base_region);
+      = make_unique<boundaries> (*m_op.m_base_region, m_logger);
 
     m_valid_region_spatial_item.add_boundaries (*result, m_logger);
     m_accessed_region_spatial_item.add_boundaries (*result, m_logger);
-    if (m_svalue_spatial_item)
-      m_svalue_spatial_item->add_boundaries (*result, m_logger);
+    if (m_written_svalue_spatial_item)
+      m_written_svalue_spatial_item->add_boundaries (*result, m_logger);
 
     return result;
   }
@@ -2324,7 +2576,7 @@  private:
 
   valid_region_spatial_item m_valid_region_spatial_item;
   accessed_region_spatial_item m_accessed_region_spatial_item;
-  std::unique_ptr<svalue_spatial_item> m_svalue_spatial_item;
+  std::unique_ptr<spatial_item> m_written_svalue_spatial_item;
 
   std::unique_ptr<boundaries> m_boundaries;
 
diff --git a/gcc/testsuite/c-c++-common/analyzer/out-of-bounds-diagram-strcat-2.c b/gcc/testsuite/c-c++-common/analyzer/out-of-bounds-diagram-strcat-2.c
new file mode 100644
index 00000000000..b129518d759
--- /dev/null
+++ b/gcc/testsuite/c-c++-common/analyzer/out-of-bounds-diagram-strcat-2.c
@@ -0,0 +1,74 @@ 
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+/* { dg-skip-if "" { powerpc-ibm-aix* } } */
+
+#include <string.h>
+
+#define LOREM_IPSUM \
+  "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod" \
+  " tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim" \
+  " veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea" \
+  " commodo consequat. Duis aute irure dolor in reprehenderit in voluptate" \
+  " velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint" \
+  " occaecat cupidatat non proident, sunt in culpa qui officia deserunt" \
+  " mollit anim id est laborum."
+
+void test (void)
+{
+  char buf[500];
+  strcpy (buf, LOREM_IPSUM);
+  strcat (buf, LOREM_IPSUM); /* { dg-warning "stack-based buffer overflow" } */
+}
+
+/* { dg-begin-multiline-output "" }
+
+                                                   ┌─────┬───┬───┬───┬───┬───┬────────┬─────┬─────┬─────┬─────┬─────┬─────┐
+                                                   │ [0] │[1]│[2]│[3]│[4]│[5]│        │[440]│[441]│[442]│[443]│[444]│[445]│
+                                                   ├─────┼───┼───┼───┼───┼───┤  ...   ├─────┼─────┼─────┼─────┼─────┼─────┤
+                                                   │ 'L' │'o'│'r'│'e'│'m'│' '│        │ 'o' │ 'r' │ 'u' │ 'm' │ '.' │ NUL │
+                                                   ├─────┴───┴───┴───┴───┴───┴────────┴─────┴─────┴─────┴─────┴─────┴─────┤
+                                                   │                  string literal (type: 'char[446]')                  │
+                                                   └──────────────────────────────────────────────────────────────────────┘
+                                                      │    │   │   │   │   │ │   │   │   │     │     │     │     │     │
+                                                      │    │   │   │   │   │ │   │   │   │     │     │     │     │     │
+                                                      v    v   v   v   v   v v   v   v   v     v     v     v     v     v
+  ┌───┬────────────────────────────────────────────┬─────┬────────────────────┬─────┐┌────────────────────────────────────┐
+  │[0]│                    ...                     │[445]│        ...         │[499]││                                    │
+  ├───┼───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬┼─────┼────────────────────┴─────┘│                                    │
+  │'L'│'o'│'r'│'e'│'m'│' '│...│'o'│'r'│'u'│'m'│'.'││ NUL │                           │         after valid range          │
+  ├───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴┴─────┴──────────────────────────┐│                                    │
+  │                            'buf' (type: 'char[500]')                            ││                                    │
+  └─────────────────────────────────────────────────────────────────────────────────┘└────────────────────────────────────┘
+  ├────────────────────────────────────────┬────────────────────────────────────────┤├─────────────────┬──────────────────┤
+                                           │                                                           │
+                                 ╭─────────┴─────────╮                                      ╭──────────┴──────────╮
+                                 │capacity: 500 bytes│                                      │overflow of 391 bytes│
+                                 ╰───────────────────╯                                      ╰─────────────────────╯
+
+   { dg-end-multiline-output "" { target c } } */
+
+/* { dg-begin-multiline-output "" }
+
+                                                   ┌─────┬───┬───┬───┬───┬───┬────────┬─────┬─────┬─────┬─────┬─────┬─────┐
+                                                   │ [0] │[1]│[2]│[3]│[4]│[5]│        │[440]│[441]│[442]│[443]│[444]│[445]│
+                                                   ├─────┼───┼───┼───┼───┼───┤  ...   ├─────┼─────┼─────┼─────┼─────┼─────┤
+                                                   │ 'L' │'o'│'r'│'e'│'m'│' '│        │ 'o' │ 'r' │ 'u' │ 'm' │ '.' │ NUL │
+                                                   ├─────┴───┴───┴───┴───┴───┴────────┴─────┴─────┴─────┴─────┴─────┴─────┤
+                                                   │               string literal (type: 'const char[446]')               │
+                                                   └──────────────────────────────────────────────────────────────────────┘
+                                                      │    │   │   │   │   │ │   │   │   │     │     │     │     │     │
+                                                      │    │   │   │   │   │ │   │   │   │     │     │     │     │     │
+                                                      v    v   v   v   v   v v   v   v   v     v     v     v     v     v
+  ┌───┬────────────────────────────────────────────┬─────┬────────────────────┬─────┐┌────────────────────────────────────┐
+  │[0]│                    ...                     │[445]│        ...         │[499]││                                    │
+  ├───┼───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬┼─────┼────────────────────┴─────┘│                                    │
+  │'L'│'o'│'r'│'e'│'m'│' '│...│'o'│'r'│'u'│'m'│'.'││ NUL │                           │         after valid range          │
+  ├───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴┴─────┴──────────────────────────┐│                                    │
+  │                      'char buf [500]' (type: 'char[500]')                       ││                                    │
+  └─────────────────────────────────────────────────────────────────────────────────┘└────────────────────────────────────┘
+  ├────────────────────────────────────────┬────────────────────────────────────────┤├─────────────────┬──────────────────┤
+                                           │                                                           │
+                                 ╭─────────┴─────────╮                                      ╭──────────┴──────────╮
+                                 │capacity: 500 bytes│                                      │overflow of 391 bytes│
+                                 ╰───────────────────╯                                      ╰─────────────────────╯
+
+   { dg-end-multiline-output "" { target c++ } } */
diff --git a/gcc/testsuite/c-c++-common/analyzer/out-of-bounds-diagram-strcat.c b/gcc/testsuite/c-c++-common/analyzer/out-of-bounds-diagram-strcat.c
new file mode 100644
index 00000000000..53c128a4fa1
--- /dev/null
+++ b/gcc/testsuite/c-c++-common/analyzer/out-of-bounds-diagram-strcat.c
@@ -0,0 +1,66 @@ 
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode -Wno-stringop-overflow" } */
+/* { dg-skip-if "" { powerpc-ibm-aix* } } */
+
+#include <string.h>
+
+void test (void)
+{
+  char buf[10];
+  strcpy (buf, "foo");
+  strcat (buf, " bar");
+  strcat (buf, " baz!"); /* { dg-warning "stack-based buffer overflow" } */
+}
+
+/* { dg-begin-multiline-output "" }
+
+                                      ┌────┬────┬────┐ ┌────┬────┬───────┐
+                                      │[0] │[1] │[2] │ │[3] │[4] │  [5]  │
+                                      ├────┼────┼────┤ ├────┼────┼───────┤
+                                      │' ' │'b' │'a' │ │'z' │'!' │  NUL  │
+                                      ├────┴────┴────┴─┴────┴────┴───────┤
+                                      │ string literal (type: 'char[6]') │
+                                      └──────────────────────────────────┘
+                                        │    │    │      │    │      │
+                                        │    │    │      │    │      │
+                                        v    v    v      v    v      v
+  ┌─────┬─────────────────────────────┬────┬────┬────┐ ┌─────────────────┐
+  │ [0] │             ...             │[7] │... │[9] │ │                 │
+  └─────┴────────┬────┬────┬────┬────┬┼────┼────┴────┘ │                 │
+                 │' ' │'b' │'a' │'r' ││NUL │           │after valid range│
+  ┌──────────────┴────┴────┴────┴────┴┴────┴─────────┐ │                 │
+  │             'buf' (type: 'char[10]')             │ │                 │
+  └──────────────────────────────────────────────────┘ └─────────────────┘
+  ├────────────────────────┬─────────────────────────┤ ├────────┬────────┤
+                           │                                    │
+                 ╭─────────┴────────╮                 ╭─────────┴─────────╮
+                 │capacity: 10 bytes│                 │overflow of 3 bytes│
+                 ╰──────────────────╯                 ╰───────────────────╯
+
+   { dg-end-multiline-output "" { target c } } */
+
+/* { dg-begin-multiline-output "" }
+
+                                  ┌─────┬─────┬─────┐  ┌─────┬─────┬─────┐
+                                  │ [0] │ [1] │ [2] │  │ [3] │ [4] │ [5] │
+                                  ├─────┼─────┼─────┤  ├─────┼─────┼─────┤
+                                  │ ' ' │ 'b' │ 'a' │  │ 'z' │ '!' │ NUL │
+                                  ├─────┴─────┴─────┴──┴─────┴─────┴─────┤
+                                  │string literal (type: 'const char[6]')│
+                                  └──────────────────────────────────────┘
+                                     │     │     │        │     │     │
+                                     │     │     │        │     │     │
+                                     v     v     v        v     v     v
+  ┌────┬──────────────────────────┬─────┬─────┬─────┐  ┌─────────────────┐
+  │[0] │           ...            │ [7] │ ... │ [9] │  │                 │
+  └────┴───────┬────┬────┬───┬───┬┼─────┼─────┴─────┘  │                 │
+               │' ' │'b' │'a'│'r'││ NUL │              │after valid range│
+  ┌────────────┴────┴────┴───┴───┴┴─────┴───────────┐  │                 │
+  │       'char buf [10]' (type: 'char[10]')        │  │                 │
+  └─────────────────────────────────────────────────┘  └─────────────────┘
+  ├────────────────────────┬────────────────────────┤  ├────────┬────────┤
+                           │                                    │
+                 ╭─────────┴────────╮                 ╭─────────┴─────────╮
+                 │capacity: 10 bytes│                 │overflow of 3 bytes│
+                 ╰──────────────────╯                 ╰───────────────────╯
+
+   { dg-end-multiline-output "" { target c++ } } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-17.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-17.c
index 6920e8c776f..d46159e56d6 100644
--- a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-17.c
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-17.c
@@ -11,19 +11,21 @@  void test (void)
 }
 
 /* { dg-begin-multiline-output "" }
-                           ┌─────┬─────┬────┬────┬────┐┌─────┬─────┬─────┐
-                           │ [0] │ [1] │[2] │[3] │[4] ││ [5] │ [6] │ [7] │
-                           ├─────┼─────┼────┼────┼────┤├─────┼─────┼─────┤
-                           │ ' ' │ 'w' │'o' │'r' │'l' ││ 'd' │ '!' │ NUL │
-                           ├─────┴─────┴────┴────┴────┴┴─────┴─────┴─────┤
-                           │      string literal (type: 'char[8]')       │
-                           └─────────────────────────────────────────────┘
-                              │     │    │    │    │      │     │     │
-                              │     │    │    │    │      │     │     │
-                              v     v    v    v    v      v     v     v
-  ┌─────┬────────────────────────────────────────┬────┐┌─────────────────┐
-  │ [0] │                  ...                   │[9] ││                 │
-  ├─────┴────────────────────────────────────────┴────┤│after valid range│
+                             ┌────┬────┬────┬────┬────┐┌─────┬─────┬─────┐
+                             │[0] │[1] │[2] │[3] │[4] ││ [5] │ [6] │ [7] │
+                             ├────┼────┼────┼────┼────┤├─────┼─────┼─────┤
+                             │' ' │'w' │'o' │'r' │'l' ││ 'd' │ '!' │ NUL │
+                             ├────┴────┴────┴────┴────┴┴─────┴─────┴─────┤
+                             │     string literal (type: 'char[8]')      │
+                             └───────────────────────────────────────────┘
+                               │    │    │    │    │      │     │     │
+                               │    │    │    │    │      │     │     │
+                               v    v    v    v    v      v     v     v
+  ┌─────┬────────────────────┬────┬──────────────┬────┐┌─────────────────┐
+  │ [0] │        ...         │[5] │     ...      │[9] ││                 │
+  ├─────┼────┬────┬────┬────┬┼────┼──────────────┴────┘│                 │
+  │ 'h' │'e' │'l' │'l' │'o' ││NUL │                    │after valid range│
+  ├─────┴────┴────┴────┴────┴┴────┴───────────────────┐│                 │
   │             'buf' (type: 'char[10]')              ││                 │
   └───────────────────────────────────────────────────┘└─────────────────┘
   ├─────────────────────────┬─────────────────────────┤├────────┬────────┤
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-18.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-18.c
index ea0b88019cd..f54cd80c338 100644
--- a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-18.c
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-18.c
@@ -11,28 +11,34 @@  void test (void)
 }
 
 /* { dg-begin-multiline-output "" }
-                             ┌─────┬─────────┐┌────┬────┬────┬────┬──────┐
-                             │ [0] │   [1]   ││[2] │[3] │[4] │[5] │ [6]  │
-                             ├─────┼─────────┤├────┼────┼────┼────┼──────┤
-                             │0xe3 │  0x83   ││0xa1│0xe3│0x82│0xa4│ 0x00 │
-                             ├─────┴─────────┴┴────┼────┴────┴────┼──────┤
-                             │       U+30e1        │    U+30a4    │U+0000│
-                             ├─────────────────────┼──────────────┼──────┤
-                             │         メ          │      イ      │ NUL  │
-                             ├─────────────────────┴──────────────┴──────┤
-                             │     string literal (type: 'char[7]')      │
-                             └───────────────────────────────────────────┘
-                                │       │       │    │    │    │     │
-                                │       │       │    │    │    │     │
-                                v       v       v    v    v    v     v
-  ┌────┬───────────────────────────┬─────────┐┌──────────────────────────┐
-  │[0] │            ...            │  [10]   ││                          │
-  ├────┴───────────────────────────┴─────────┤│    after valid range     │
-  │         'buf' (type: 'char[11]')         ││                          │
-  └──────────────────────────────────────────┘└──────────────────────────┘
-  ├────────────────────┬─────────────────────┤├────────────┬─────────────┤
-                       │                                   │
-             ╭─────────┴────────╮                ╭─────────┴─────────╮
-             │capacity: 11 bytes│                │overflow of 5 bytes│
-             ╰──────────────────╯                ╰───────────────────╯
+                                                ┌──────┬────┐┌────┬────┬────┬────┬──────┐
+                                                │ [0]  │[1] ││[2] │[3] │[4] │[5] │ [6]  │
+                                                ├──────┼────┤├────┼────┼────┼────┼──────┤
+                                                │ 0xe3 │0x83││0xa1│0xe3│0x82│0xa4│ 0x00 │
+                                                ├──────┴────┴┴────┼────┴────┴────┼──────┤
+                                                │     U+30e1      │    U+30a4    │U+0000│
+                                                ├─────────────────┼──────────────┼──────┤
+                                                │       メ        │      イ      │ NUL  │
+                                                ├─────────────────┴──────────────┴──────┤
+                                                │   string literal (type: 'char[7]')    │
+                                                └───────────────────────────────────────┘
+                                                   │     │     │    │    │    │     │
+                                                   │     │     │    │    │    │     │
+                                                   v     v     v    v    v    v     v
+  ┌────┬────────────────────────────────────────┬──────┬────┐┌──────────────────────────┐
+  │[0] │                  ...                   │ [9]  │[10]││                          │
+  ├────┼────┬────┬────┬────┬────┬────┬────┬────┬┼──────┼────┘│                          │
+  │0xe3│0x82│0xb5│0xe3│0x83│0x84│0xe3│0x82│0xad││ 0x00 │     │                          │
+  ├────┴────┴────┼────┴────┴────┼────┴────┴────┤├──────┤     │                          │
+  │    U+30b5    │    U+30c4    │    U+30ad    ││U+0000│     │    after valid range     │
+  ├──────────────┼──────────────┼──────────────┤├──────┤     │                          │
+  │      サ      │      ツ      │      キ      ││ NUL  │     │                          │
+  ├──────────────┴──────────────┴──────────────┴┴──────┴────┐│                          │
+  │                'buf' (type: 'char[11]')                 ││                          │
+  └─────────────────────────────────────────────────────────┘└──────────────────────────┘
+  ├────────────────────────────┬────────────────────────────┤├────────────┬─────────────┤
+                               │                                          │
+                     ╭─────────┴────────╮                       ╭─────────┴─────────╮
+                     │capacity: 11 bytes│                       │overflow of 5 bytes│
+                     ╰──────────────────╯                       ╰───────────────────╯
    { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-19.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-19.c
index 35ab72b6efc..6af5c0f301b 100644
--- a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-19.c
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-19.c
@@ -22,24 +22,26 @@  test_long_string ()
 }
 
 /* { dg-begin-multiline-output "" }
-        ┌───┬───┬───┬───┬───┬───┬───────┬─────┬─────┬─────┬─────┬─────┬─────┐
-        │[0]│[1]│[2]│[3]│[4]│[5]│       │[440]│[441]│[442]│[443]│[444]│[445]│
-        ├───┼───┼───┼───┼───┼───┤  ...  ├─────┼─────┼─────┼─────┼─────┼─────┤
-        │'L'│'o'│'r'│'e'│'m'│' '│       │ 'o' │ 'r' │ 'u' │ 'm' │ '.' │ NUL │
-        ├───┴───┴───┴───┴───┴───┴───────┴─────┴─────┴─────┴─────┴─────┴─────┤
-        │                string literal (type: 'char[446]')                 │
-        └───────────────────────────────────────────────────────────────────┘
-          │   │   │   │   │   │ │  │   │   │     │     │     │     │     │
-          │   │   │   │   │   │ │  │   │   │     │     │     │     │     │
-          v   v   v   v   v   v v  v   v   v     v     v     v     v     v
-  ┌───┬──────────────────────────┬────┐┌────────────────────────────────────┐
-  │[0]│           ...            │[99]││                                    │
-  ├───┴──────────────────────────┴────┤│         after valid range          │
-  │     'buf' (type: 'char[100]')     ││                                    │
-  └───────────────────────────────────┘└────────────────────────────────────┘
-  ├─────────────────┬─────────────────┤├─────────────────┬──────────────────┤
-                    │                                    │
-          ╭─────────┴─────────╮               ╭──────────┴──────────╮
-          │capacity: 100 bytes│               │overflow of 350 bytes│
-          ╰───────────────────╯               ╰─────────────────────╯
+                   ┌───┬───┬───┬───┬───┬───┬───────┬─────┬─────┬─────┬─────┬─────┬─────┐
+                   │[0]│[1]│[2]│[3]│[4]│[5]│       │[440]│[441]│[442]│[443]│[444]│[445]│
+                   ├───┼───┼───┼───┼───┼───┤  ...  ├─────┼─────┼─────┼─────┼─────┼─────┤
+                   │'L'│'o'│'r'│'e'│'m'│' '│       │ 'o' │ 'r' │ 'u' │ 'm' │ '.' │ NUL │
+                   ├───┴───┴───┴───┴───┴───┴───────┴─────┴─────┴─────┴─────┴─────┴─────┤
+                   │                string literal (type: 'char[446]')                 │
+                   └───────────────────────────────────────────────────────────────────┘
+                     │   │   │   │   │   │ │  │   │   │     │     │     │     │     │
+                     │   │   │   │   │   │ │  │   │   │     │     │     │     │     │
+                     v   v   v   v   v   v v  v   v   v     v     v     v     v     v
+  ┌───┬────────────┬───┬────────────────────┬────┐┌────────────────────────────────────┐
+  │[0]│    ...     │[4]│        ...         │[99]││                                    │
+  ├───┼───┬───┬───┬┼───┼────────────────────┴────┘│                                    │
+  │'a'│'b'│'c'│' '││NUL│                          │         after valid range          │
+  ├───┴───┴───┴───┴┴───┴─────────────────────────┐│                                    │
+  │          'buf' (type: 'char[100]')           ││                                    │
+  └──────────────────────────────────────────────┘└────────────────────────────────────┘
+  ├──────────────────────┬───────────────────────┤├─────────────────┬──────────────────┤
+                         │                                          │
+               ╭─────────┴─────────╮                     ╭──────────┴──────────╮
+               │capacity: 100 bytes│                     │overflow of 350 bytes│
+               ╰───────────────────╯                     ╰─────────────────────╯
    { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-6.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-6.c
index 25bf9d53b2b..ad320964057 100644
--- a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-6.c
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-6.c
@@ -33,22 +33,24 @@  test_bad_memcpy ()
 
 /* { dg-begin-multiline-output "" }
 
-  ┌─────────────────────────────────────────────────────────────────────────┐
-  │                           read of 4096 bytes                            │
-  └─────────────────────────────────────────────────────────────────────────┘
-           ^               ^          ^           ^                ^
-           │               │          │           │                │
-           │               │          │           │                │
-  ┌──────────────────┐┌──────────┬──────────┬────────────┐┌─────────────────┐
-  │                  ││   [0]    │   ...    │   [445]    ││                 │
-  │before valid range│├──────────┴──────────┴────────────┤│after valid range│
-  │                  ││string literal (type: 'char[446]')││                 │
-  └──────────────────┘└──────────────────────────────────┘└─────────────────┘
-  ├────────┬─────────┤├────────────────┬─────────────────┤├────────┬────────┤
-           │                           │                           │
-  ╭────────┴──────────────╮    ╭───────┴───────╮       ╭───────────┴───────────╮
-  │under-read of 100 bytes│    │size: 446 bytes│       │over-read of 3550 bytes│
-  ╰───────────────────────╯    ╰───────────────╯       ╰───────────────────────╯
+  ┌────────────────────────────────────────────────────────────────────────────────────────────┐
+  │                                     read of 4096 bytes                                     │
+  └────────────────────────────────────────────────────────────────────────────────────────────┘
+           ^            ^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^    ^            ^
+           │            │   │   │   │   │   │   │   │   │   │   │   │    │            │
+           │            │   │   │   │   │   │   │   │   │   │   │   │    │            │
+  ┌──────────────────┐┌───┬───────────────────────────────────────────┬─────┐┌─────────────────┐
+  │                  ││[0]│                    ...                    │[445]││                 │
+  │                  │├───┼───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┼─────┤│                 │
+  │before valid range││'L'│'o'│'r'│'e'│'m'│' '│...│'o'│'r'│'u'│'m'│'.'│ NUL ││after valid range│
+  │                  │├───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴─────┤│                 │
+  │                  ││         string literal (type: 'char[446]')          ││                 │
+  └──────────────────┘└─────────────────────────────────────────────────────┘└─────────────────┘
+  ├────────┬─────────┤├──────────────────────────┬──────────────────────────┤├────────┬────────┤
+           │                                     │                                    │
+  ╭────────┴──────────────╮              ╭───────┴───────╮                ╭───────────┴───────────╮
+  │under-read of 100 bytes│              │size: 446 bytes│                │over-read of 3550 bytes│
+  ╰───────────────────────╯              ╰───────────────╯                ╰───────────────────────╯
 
    { dg-end-multiline-output "" } */
 
@@ -81,22 +83,24 @@  test_bad_memcpy ()
 
 /* { dg-begin-multiline-output "" }
 
-  ┌─────────────────────────────────────────────────────────────────────────┐
-  │                           read of 4096 bytes                            │
-  └─────────────────────────────────────────────────────────────────────────┘
-           ^               ^          ^           ^                ^
-           │               │          │           │                │
-           │               │          │           │                │
-  ┌──────────────────┐┌──────────┬──────────┬────────────┐┌─────────────────┐
-  │                  ││   [0]    │   ...    │   [445]    ││                 │
-  │before valid range│├──────────┴──────────┴────────────┤│after valid range│
-  │                  ││string literal (type: 'char[446]')││                 │
-  └──────────────────┘└──────────────────────────────────┘└─────────────────┘
-  ├────────┬─────────┤├────────────────┬─────────────────┤├────────┬────────┤
-           │                           │                           │
-  ╭────────┴──────────────╮    ╭───────┴───────╮       ╭───────────┴───────────╮
-  │under-read of 100 bytes│    │size: 446 bytes│       │over-read of 3550 bytes│
-  ╰───────────────────────╯    ╰───────────────╯       ╰───────────────────────╯
+  ┌────────────────────────────────────────────────────────────────────────────────────────────┐
+  │                                     read of 4096 bytes                                     │
+  └────────────────────────────────────────────────────────────────────────────────────────────┘
+           ^            ^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^   ^    ^            ^
+           │            │   │   │   │   │   │   │   │   │   │   │   │    │            │
+           │            │   │   │   │   │   │   │   │   │   │   │   │    │            │
+  ┌──────────────────┐┌───┬───────────────────────────────────────────┬─────┐┌─────────────────┐
+  │                  ││[0]│                    ...                    │[445]││                 │
+  │                  │├───┼───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┼─────┤│                 │
+  │before valid range││'L'│'o'│'r'│'e'│'m'│' '│...│'o'│'r'│'u'│'m'│'.'│ NUL ││after valid range│
+  │                  │├───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴─────┤│                 │
+  │                  ││         string literal (type: 'char[446]')          ││                 │
+  └──────────────────┘└─────────────────────────────────────────────────────┘└─────────────────┘
+  ├────────┬─────────┤├──────────────────────────┬──────────────────────────┤├────────┬────────┤
+           │                                     │                                    │
+  ╭────────┴──────────────╮              ╭───────┴───────╮                ╭───────────┴───────────╮
+  │under-read of 100 bytes│              │size: 446 bytes│                │over-read of 3550 bytes│
+  ╰───────────────────────╯              ╰───────────────╯                ╰───────────────────────╯
 
    { dg-end-multiline-output "" } */
 
diff --git a/gcc/text-art/table.cc b/gcc/text-art/table.cc
index 2f857a0e2a7..2ea0da3a3d8 100644
--- a/gcc/text-art/table.cc
+++ b/gcc/text-art/table.cc
@@ -150,6 +150,26 @@  table::set_cell_span (rect_t span,
       }
 }
 
+/* If SPAN is unoccuped, set it to CONTENT.
+   Otherwise, discard CONTENT.  */
+
+void
+table::maybe_set_cell_span (rect_t span,
+			    table_cell_content &&content,
+			    enum x_align x_align,
+			    enum y_align y_align)
+{
+  gcc_assert (span.m_size.w > 0);
+  gcc_assert (span.m_size.h > 0);
+  for (int y = span.get_min_y (); y < span.get_next_y (); y++)
+    for (int x = span.get_min_x (); x < span.get_next_x (); x++)
+      {
+	if (m_occupancy.get (coord_t (x, y)) != -1)
+	  return;
+      }
+  set_cell_span (span, std::move (content), x_align, y_align);
+}
+
 canvas
 table::to_canvas (const theme &theme, const style_manager &sm) const
 {
@@ -189,6 +209,21 @@  table::debug () const
   canvas.debug (false);
 }
 
+/* Move OTHER's content this table, starting at OFFSET.  */
+
+void
+table::add_other_table (table &&other,
+			table::coord_t offset)
+{
+  for (auto &&placement : other.m_placements)
+    {
+      set_cell_span (placement.m_rect + offset,
+		     std::move (placement.m_content),
+		     placement.m_x_align,
+		     placement.m_y_align);
+    }
+}
+
 const table::cell_placement *
 table::get_placement_at (coord_t coord) const
 {
diff --git a/gcc/text-art/table.h b/gcc/text-art/table.h
index 17eda912f1a..5d5d4bdde1f 100644
--- a/gcc/text-art/table.h
+++ b/gcc/text-art/table.h
@@ -115,6 +115,7 @@  class table
     const table_cell_content &get_content () const { return m_content; }
 
   private:
+    friend class table;
     friend class table_cell_sizes;
     rect_t m_rect;
     table_cell_content m_content;
@@ -130,11 +131,18 @@  class table
 
   const size_t &get_size () const { return m_size; }
 
+  int add_rows (unsigned num)
+  {
+    int topmost_new_row = m_size.h;
+    m_size.h += num;
+    for (unsigned i = 0; i < num; i++)
+      m_occupancy.add_row (-1);
+    return topmost_new_row;
+  }
+
   int add_row ()
   {
-    m_size.h++;
-    m_occupancy.add_row (-1);
-    return m_size.h - 1; // return the table_y of the newly-added row
+    return add_rows (1);
   }
 
   void set_cell (coord_t coord,
@@ -147,6 +155,11 @@  class table
 		      enum x_align x_align = x_align::CENTER,
 		      enum y_align y_align = y_align::CENTER);
 
+  void maybe_set_cell_span (rect_t span,
+			    table_cell_content &&content,
+			    enum x_align x_align = x_align::CENTER,
+			    enum y_align y_align = y_align::CENTER);
+
   canvas to_canvas (const theme &theme, const style_manager &sm) const;
 
   void paint_to_canvas(canvas &canvas,
@@ -156,6 +169,8 @@  class table
 
   void debug () const;
 
+  void add_other_table (table &&other, table::coord_t offset);
+
   /* Self-test support.  */
   const cell_placement *get_placement_at (coord_t coord) const;
 
diff --git a/gcc/text-art/types.h b/gcc/text-art/types.h
index ea4ff4be8cc..d5394a92d6f 100644
--- a/gcc/text-art/types.h
+++ b/gcc/text-art/types.h
@@ -129,6 +129,13 @@  struct rect
   size<CoordinateSystem> m_size;
 };
 
+template <typename CoordinateSystem>
+rect<CoordinateSystem> operator+ (rect<CoordinateSystem> r,
+				  coord<CoordinateSystem> offset)
+{
+  return rect<CoordinateSystem> (r.m_top_left + offset, r.m_size);
+}
+
 template <typename ElementType, typename SizeType, typename CoordType>
 class array2
 {