From: Jonas Hansen <[email protected]>

The call to braced_lists_to_strings in store_init_value does not look
through TARGET_EXPR. A constexpr aggregate initialized through one can
therefore retain a character array CONSTRUCTOR after constant evaluation.
The middle end then treats an element of that array as a single-character
string, causing a false -Wformat-truncation warning for a bounded snprintf
call that reads only the remaining array elements.

Move the existing conversion after constant folding, next to
replace_placeholders, so it sees the folded initializer while remaining
before split_nonconstant_init. Add diagnostic and runtime regressions
covering bounded reads, overreads, truncation, constexpr copy and mutation,
templates, embedded nuls, and implicit zero initialization.

Tested on x86_64-pc-linux-gnu with related C++ format, initialization,
constexpr, narrowing, template, string and #embed tests: 14,435 expected
passes and no unexpected results across two test selections. C++ self-tests
and the runtime regression, including LTO, also passed. A full bootstrap
and complete testsuite run have not been performed.

gcc/cp/ChangeLog:

        PR tree-optimization/127416
        * typeck2.cc (store_init_value): Move braced_lists_to_strings after
        constant folding.

gcc/testsuite/ChangeLog:

        PR tree-optimization/127416
        * g++.dg/warn/Wformat-truncation-nonstring.C: New test.
        * g++.dg/torture/constexpr-array-string.C: New test.
---
Fixes [GCC PR 
tree-optimization/127416](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127416).

Compiling the following with `g++ -Wall -O1` produces a false
`-Wformat-truncation` warning that the `%.*s` argument is not nul-terminated:

```cpp
#include <cstdio>

struct Buffer { char data[3]; };
constexpr auto buffer = Buffer{{'a', 'b', 'c'}};

int main()
{
  char out[3];
  return std::snprintf(out, sizeof out, "%.*s", 2, buffer.data + 1) != 2;
}
```

The precision limits the read to the two remaining array elements, so the
conversion does not require a terminating nul in the source array.

In `store_init_value`, the existing `braced_lists_to_strings` call returns
without looking through a `TARGET_EXPR`. Constant evaluation later exposes
a character-array `CONSTRUCTOR`, which the middle end handles as a
single-character string when examining the nonzero offset. Move the existing
call after constant folding, immediately after `replace_placeholders`, so it
sees the folded initializer. The conversion remains before
`split_nonconstant_init`.

Add tests for safe bounded reads, genuine overreads and truncation, constexpr
copying and mutation, multiple template instantiations, embedded nuls, and
implicit zero initialization.

Validation on x86_64-pc-linux-gnu:

- Rebuilt `cc1plus`; the reproducer compiles without diagnostics under
  `-Wall -Wextra -Werror` at `-O0`, `-O1`, `-O2`, `-O3`, and `-Os`, and each
  resulting program exits successfully.
- Related C++ format, initialization, and constexpr tests: 10,368 expected
  passes, 27 expected failures, 20 unsupported tests, and no unexpected results.
- Additional narrowing, NSDMI, char8_t, class non-type template argument,
  string/array warning and `#embed` tests: 4,067 expected passes, 83 expected
  failures, 12 unsupported tests, and no unexpected results.
- New runtime regression passes at all tested optimization levels, including
  two LTO configurations.
- C++ self-tests: 7,665,915 passes. GCC ChangeLog and style checks pass.

A full bootstrap with all default languages, the complete testsuite, and
`check-c++-all` remain outstanding. This proposal is available for initial
review; the results above should not be read as a completed upstream
validation run.

## Changes since v1

- Following review on gcc-patches, move the existing
  `braced_lists_to_strings` call after constant folding, beside
  `replace_placeholders`, instead of adding a second call at `DECL_INITIAL`.
- Keep the two regression tests unchanged.
- Recheck the existing regression selection and run additional existing tests 
for
  narrowing, NSDMI, char8_t, class template arguments, string diagnostics,
  and `#embed`.



This is a forge pull request published on the gcc-patches mailing list mailing 
list
as requested by Jonas Hansen via Sourceware Forge 
<[email protected]>.
Forge discussion: https://forge.sourceware.org/gcc/gcc/pulls/232

Get it locally using:
```
git fetch forge-upstream "+refs/versioned_pull/232/*:refs/versioned_pull/232/*"
git switch -c "pr-232-v2" "refs/versioned_pull/232/2/head"
```
Or, download the patch at: https://forge.sourceware.org/gcc/gcc/pulls/232.diff
Created on: 2026-09-23 14:02:09+00:00
Latest update: 2026-09-28 08:17:32+00:00
Changes: 3 changed files, 108 additions, 4 deletions
Head revision: hrjonashansen/gcc ref fix-bounded-string-warning commit 
24d0ebe268b688ce68dea87d7b581df82bddf8e7 
Base revision: gcc/gcc ref trunk commit 
1743eb860b739c0bae408536d6a9459f2325fe18 r17-4593-g1743eb860b739c
Merge base: 1743eb860b739c0bae408536d6a9459f2325fe18
Requested Reviewers: jason

Changed files:
- A: gcc/testsuite/g++.dg/torture/constexpr-array-string.C
- A: gcc/testsuite/g++.dg/warn/Wformat-truncation-nonstring.C
- M: gcc/cp/typeck2.cc

 gcc/cp/typeck2.cc                             |  8 +--
 .../g++.dg/torture/constexpr-array-string.C   | 56 +++++++++++++++++++
 .../warn/Wformat-truncation-nonstring.C       | 48 ++++++++++++++++
 3 files changed, 108 insertions(+), 4 deletions(-)
 create mode 100644 gcc/testsuite/g++.dg/torture/constexpr-array-string.C
 create mode 100644 gcc/testsuite/g++.dg/warn/Wformat-truncation-nonstring.C

diff --git a/gcc/cp/typeck2.cc b/gcc/cp/typeck2.cc
index 6f17ce8c1bc9c..c7ebd080edc6a 100644
--- a/gcc/cp/typeck2.cc
+++ b/gcc/cp/typeck2.cc
@@ -1056,10 +1056,6 @@ store_init_value (tree decl, tree init, vec<tree, 
va_gc>** cleanups, int flags)
       value = digest_init_flags (type, init, flags, tf_warning_or_error);
     }
 
-  /* Look for braced array initializers for character arrays and
-     recursively convert them into STRING_CSTs.  */
-  value = braced_lists_to_strings (type, value);
-
   current_ref_temp_count = 0;
   value = extend_ref_init_temps (decl, value, cleanups);
 
@@ -1139,6 +1135,10 @@ store_init_value (tree decl, tree init, vec<tree, 
va_gc>** cleanups, int flags)
   /* Handle aggregate NSDMI in non-constant initializers, too.  */
   value = replace_placeholders (value, decl);
 
+  /* Look for braced array initializers for character arrays and
+     recursively convert them into STRING_CSTs.  */
+  value = braced_lists_to_strings (type, value);
+
   /* Detect stuff like 'info r = ^^int;' outside a manifestly
      constant-evaluated context.  */
   if (flag_reflection
diff --git a/gcc/testsuite/g++.dg/torture/constexpr-array-string.C 
b/gcc/testsuite/g++.dg/torture/constexpr-array-string.C
new file mode 100644
index 0000000000000..bc0829af53106
--- /dev/null
+++ b/gcc/testsuite/g++.dg/torture/constexpr-array-string.C
@@ -0,0 +1,56 @@
+// Check character arrays produced by constant evaluation, including copies,
+// mutation, embedded nuls, and implicit zero initialization.
+// { dg-do run { target c++14 } }
+
+struct Buffer { char data[3]; };
+constexpr auto original = Buffer{{'a', 'b', 'c'}};
+
+constexpr Buffer make_buffer ()
+{
+  Buffer b = original;
+  b.data[1] = 'x';
+  return b;
+}
+
+constexpr auto changed = make_buffer ();
+constexpr auto embedded = Buffer{{'a', '\0', 'c'}};
+constexpr auto padded = Buffer{{'a'}};
+
+template <char C> struct Holder
+{
+  static constexpr Buffer value = Buffer{{'a', C, 'c'}};
+};
+template <char C> constexpr Buffer Holder<C>::value;
+
+static_assert (original.data[1] == 'b', "");
+static_assert (changed.data[1] == 'x', "");
+static_assert (embedded.data[2] == 'c', "");
+static_assert (padded.data[2] == '\0', "");
+static_assert (Holder<'y'>::value.data[1] == 'y', "");
+static_assert (Holder<'z'>::value.data[1] == 'z', "");
+
+void check (const char *data, int precision, const char *expected, int length)
+{
+  char out[4];
+  if (__builtin_snprintf (out, sizeof out, "%.*s", precision, data) != length
+      || __builtin_memcmp (out, expected, length + 1))
+    __builtin_abort ();
+}
+
+int main ()
+{
+  check (original.data + 1, 2, "bc", 2);
+  check (changed.data + 1, 2, "xc", 2);
+  check (embedded.data, 3, "a", 1);
+  check (embedded.data + 1, 2, "", 0);
+  check (embedded.data + 2, 1, "c", 1);
+  check (padded.data, 3, "a", 1);
+  check (padded.data + 1, 2, "", 0);
+  check (Holder<'y'>::value.data + 1, 2, "yc", 2);
+  check (Holder<'z'>::value.data + 1, 2, "zc", 2);
+
+  Buffer copy = original;
+  copy.data[1] = 'z';
+  check (copy.data + 1, 2, "zc", 2);
+  check (original.data + 1, 2, "bc", 2);
+}
diff --git a/gcc/testsuite/g++.dg/warn/Wformat-truncation-nonstring.C 
b/gcc/testsuite/g++.dg/warn/Wformat-truncation-nonstring.C
new file mode 100644
index 0000000000000..759675a43cfa7
--- /dev/null
+++ b/gcc/testsuite/g++.dg/warn/Wformat-truncation-nonstring.C
@@ -0,0 +1,48 @@
+// A bounded %s conversion need not read a terminating nul.
+// { dg-do compile { target c++11 } }
+// { dg-options "-O1 -Wall" }
+
+struct Buffer { char data[3]; };
+constexpr auto buffer = Buffer{{'a', 'b', 'c'}};
+
+int bounded ()
+{
+  char out[3];
+  return __builtin_snprintf (out, sizeof out, "%.*s", 2,
+                            buffer.data + 1); // { dg-bogus "nul-terminated" 
"" { target *-*-* } .-1 }
+}
+
+int bounded_literal ()
+{
+  char out[3];
+  return __builtin_snprintf (out, sizeof out, "%.2s",
+                            buffer.data + 1); // { dg-bogus "nul-terminated" 
"" { target *-*-* } .-1 }
+}
+
+int last_byte ()
+{
+  char out[2];
+  return __builtin_snprintf (out, sizeof out, "%.*s", 1,
+                            buffer.data + 2);
+}
+
+int overread ()
+{
+  char out[8];
+  return __builtin_snprintf (out, sizeof out, "%.*s", 3,
+                            buffer.data + 1); // { dg-warning "nul-terminated" 
"" { target *-*-* } .-1 }
+}
+
+int unbounded ()
+{
+  char out[8];
+  return __builtin_snprintf (out, sizeof out, "%s",
+                            buffer.data + 1); // { dg-warning "nul-terminated" 
"" { target *-*-* } .-1 }
+}
+
+int truncated ()
+{
+  char out[2];
+  return __builtin_snprintf (out, sizeof out, "%.*s", 2,
+                            buffer.data + 1); // { dg-warning "truncated" "" { 
target *-*-* } .-1 }
+}

Range-diff against v1:
1:  0a3e5684d37f9 ! 1:  24d0ebe268b68 c++: Normalize character array 
initializers after folding [PR127416]
    @@ Metadata
      ## Commit message ##
         c++: Normalize character array initializers after folding [PR127416]
     
    -    Constant evaluation can expose braced character array initializers 
after
    -    the initial call to braced_lists_to_strings. For example, a constexpr
    -    aggregate initialized through a TARGET_EXPR can retain a character 
array
    -    CONSTRUCTOR. The middle end then treats an element of that array as a
    -    single-character string, causing a false -Wformat-truncation warning 
for
    -    a bounded snprintf call that reads only the remaining array elements.
    +    The call to braced_lists_to_strings in store_init_value does not look
    +    through TARGET_EXPR. A constexpr aggregate initialized through one can
    +    therefore retain a character array CONSTRUCTOR after constant 
evaluation.
    +    The middle end then treats an element of that array as a 
single-character
    +    string, causing a false -Wformat-truncation warning for a bounded 
snprintf
    +    call that reads only the remaining array elements.
     
    -    Normalize the final initializer when assigning DECL_INITIAL, retaining
    -    the existing early conversion. Add diagnostic and runtime regressions
    +    Move the existing conversion after constant folding, next to
    +    replace_placeholders, so it sees the folded initializer while remaining
    +    before split_nonconstant_init. Add diagnostic and runtime regressions
         covering bounded reads, overreads, truncation, constexpr copy and 
mutation,
         templates, embedded nuls, and implicit zero initialization.
     
    -    Tested on x86_64-pc-linux-gnu with the related C++ format, 
initialization,
    -    and constexpr tests: 10,368 expected passes and no unexpected failures.
    -    C++ self-tests and the runtime regression, including LTO, also passed.
    -    A full bootstrap and complete testsuite run have not been performed.
    +    Tested on x86_64-pc-linux-gnu with related C++ format, initialization,
    +    constexpr, narrowing, template, string and #embed tests: 14,435 
expected
    +    passes and no unexpected results across two test selections. C++ 
self-tests
    +    and the runtime regression, including LTO, also passed. A full 
bootstrap
    +    and complete testsuite run have not been performed.
     
         gcc/cp/ChangeLog:
     
                 PR tree-optimization/127416
    -            * typeck2.cc (store_init_value): Convert braced character array
    -            initializers to strings after folding.
    +            * typeck2.cc (store_init_value): Move braced_lists_to_strings 
after
    +            constant folding.
     
         gcc/testsuite/ChangeLog:
     
    @@ Commit message
     
      ## gcc/cp/typeck2.cc ##
     @@ gcc/cp/typeck2.cc: store_init_value (tree decl, tree init, vec<tree, 
va_gc>** cleanups, int flags)
    +       value = digest_init_flags (type, init, flags, tf_warning_or_error);
    +     }
      
    -   /* If the value is a constant, just put it in DECL_INITIAL.  If DECL
    -      is an automatic variable, the middle end will turn this into a
    --     dynamic initialization later.  */
    --  DECL_INITIAL (decl) = value;
    -+     dynamic initialization later.  Folding may have exposed more braced
    -+     character array initializers, so convert those to STRING_CSTs too.  
*/
    -+  DECL_INITIAL (decl) = braced_lists_to_strings (type, value);
    -   return NULL_TREE;
    - }
    +-  /* Look for braced array initializers for character arrays and
    +-     recursively convert them into STRING_CSTs.  */
    +-  value = braced_lists_to_strings (type, value);
    +-
    +   current_ref_temp_count = 0;
    +   value = extend_ref_init_temps (decl, value, cleanups);
      
    +@@ gcc/cp/typeck2.cc: store_init_value (tree decl, tree init, vec<tree, 
va_gc>** cleanups, int flags)
    +   /* Handle aggregate NSDMI in non-constant initializers, too.  */
    +   value = replace_placeholders (value, decl);
    + 
    ++  /* Look for braced array initializers for character arrays and
    ++     recursively convert them into STRING_CSTs.  */
    ++  value = braced_lists_to_strings (type, value);
    ++
    +   /* Detect stuff like 'info r = ^^int;' outside a manifestly
    +      constant-evaluated context.  */
    +   if (flag_reflection
     
      ## gcc/testsuite/g++.dg/torture/constexpr-array-string.C (new) ##
     @@
-- 
2.54.0

Reply via email to