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