Skip to content

GCC 16.1.0 dead store elimination causes uninitialized memory accesses #1157

@jeremiahar

Description

@jeremiahar

It seems that GCC 16.1.0 breaks boost::json. Valgrind shows accesses to uninitialized memory on simple examples such as:

#include <boost/json/src.hpp>
#include <string>
#include <iostream>

int main(int argc, char **argv)
{
    boost::json::value document;

    boost::json::object &object = document.emplace_object();
    std::string tmp = boost::json::serialize(document);
    std::cout << tmp << std::endl;
    return 0;
}

Example compilation:

[root@10d5486a3e37 ~]# g++ -std=c++23 -g3 -O3 -I/root/boost_1_91_0/  test.cpp -o test && valgrind --tool=memcheck --track-origins=yes ./test
==216== Memcheck, a memory error detector
==216== Copyright (C) 2002-2026, and GNU GPL'd, by Julian Seward et al.
==216== Using Valgrind-3.27.0 and LibVEX; rerun with -h for copyright info
==216== Command: ./test
==216== 
==216== Conditional jump or move depends on uninitialised value(s)
==216==    at 0x408A97: boost::json::value::destroy() (value.ipp:811)
==216==    by 0x402463: emplace_object (value.ipp:654)
==216==    by 0x402463: main (test.cpp:9)
==216==  Uninitialised value was created by a stack allocation
==216==    at 0x402450: main (test.cpp:6)
==216== 
==216== Conditional jump or move depends on uninitialised value(s)
==216==    at 0x408A9F: boost::json::value::destroy() (value.ipp:811)
==216==    by 0x402463: emplace_object (value.ipp:654)
==216==    by 0x402463: main (test.cpp:9)
==216==  Uninitialised value was created by a stack allocation
==216==    at 0x402450: main (test.cpp:6)
==216== 
==216== Conditional jump or move depends on uninitialised value(s)
==216==    at 0x408AA7: boost::json::value::destroy() (value.ipp:811)
==216==    by 0x402463: emplace_object (value.ipp:654)
==216==    by 0x402463: main (test.cpp:9)
==216==  Uninitialised value was created by a stack allocation
==216==    at 0x402450: main (test.cpp:6)
==216== 
{}
==216== Conditional jump or move depends on uninitialised value(s)
==216==    at 0x4081CD: boost::json::object::~object() (object.ipp:293)
==216==    by 0x402504: main (test.cpp:13)
==216==  Uninitialised value was created by a stack allocation
==216==    at 0x402450: main (test.cpp:6)
==216== 
==216== Conditional jump or move depends on uninitialised value(s)
==216==    at 0x4081DC: release (storage_ptr.hpp:107)
==216==    by 0x4081DC: ~storage_ptr (storage_ptr.hpp:141)
==216==    by 0x4081DC: boost::json::object::~object() (object.ipp:298)
==216==    by 0x402504: main (test.cpp:13)
==216==  Uninitialised value was created by a stack allocation
==216==    at 0x402450: main (test.cpp:6)
==216== 
==216== 
==216== HEAP SUMMARY:
==216==     in use at exit: 0 bytes in 0 blocks
==216==   total heap usage: 2 allocs, 2 frees, 74,752 bytes allocated
==216== 
==216== All heap blocks were freed -- no leaks are possible
==216== 
==216== For lists of detected and suppressed errors, rerun with: -s
==216== ERROR SUMMARY: 5 errors from 5 contexts (suppressed: 0 from 0)

Disassembly also confirms that the value really is uninitialized:

objdump -d -M intel --source --disassemble=main --demangle test

int main(int argc, char **argv)
{
  402450:       53                      push   rbx
  402451:       48 83 ec 60             sub    rsp,0x60

object&
value::
emplace_object() noexcept
{
    return *::new(&obj_) object(destroy());
  402455:       48 8d 74 24 20          lea    rsi,[rsp+0x20]
  40245a:       48 8d 7c 24 40          lea    rdi,[rsp+0x40]
  40245f:       e8 1c 66 00 00          call   408a80 <boost::json::value::destroy()>


I have done some testing and apparently passing -fno-lifetime-dse eliminates the problem:

[root@10d5486a3e37 ~]# g++ -std=c++23 -g3 -O3 -fno-lifetime-dse -I/root/boost_1_91_0/  test.cpp -o test && valgrind --tool=memcheck --track-origins=yes ./test
==223== Memcheck, a memory error detector
==223== Copyright (C) 2002-2026, and GNU GPL'd, by Julian Seward et al.
==223== Using Valgrind-3.27.0 and LibVEX; rerun with -h for copyright info
==223== Command: ./test
==223== 
{}
==223== 
==223== HEAP SUMMARY:
==223==     in use at exit: 0 bytes in 0 blocks
==223==   total heap usage: 2 allocs, 2 frees, 74,752 bytes allocated
==223== 
==223== All heap blocks were freed -- no leaks are possible
==223== 
==223== For lists of detected and suppressed errors, rerun with: -s
==223== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Disassembly confirms sp and k are properly initialized with -fno-lifetime-dse:

int main(int argc, char **argv)
{
  4023a0:       53                      push   rbx
  4023a1:       48 83 ec 60             sub    rsp,0x60

object&
value::
emplace_object() noexcept
{
    return *::new(&obj_) object(destroy());
  4023a5:       48 8d 74 24 20          lea    rsi,[rsp+0x20]
  4023aa:       48 8d 7c 24 40          lea    rdi,[rsp+0x40]
    };

    explicit
    scalar(storage_ptr sp_ = {}) noexcept
        : sp(std::move(sp_))
        , k(json::kind::null)
  4023af:       c6 44 24 28 00          mov    BYTE PTR [rsp+0x28],0x0

        @param other Another pointer.
    */
    storage_ptr(
        storage_ptr&& other) noexcept
        : i_(detail::exchange(other.i_, 0))
  4023b4:       48 c7 44 24 20 00 00    mov    QWORD PTR [rsp+0x20],0x0
  4023bb:       00 00 
  4023bd:       e8 5e 66 00 00          call   408a20 <boost::json::value::destroy()>

Full test log on Fedora 44:

[jeremiah@jeremiah json]$ docker run -it fedora:44
[root@10d5486a3e37 /]# dnf install -q -y gcc g++ valgrind wget &> /dev/null
[root@10d5486a3e37 /]# cd /root
[root@10d5486a3e37 ~]# wget https://archives.boost.io/release/1.91.0/source/boost_1_91_0.tar.gz
Saving 'boost_1_91_0.tar.gz'
HTTP response 200  [https://archives.boost.io/release/1.91.0/source/boost_1_91_0.tar.gz]
boost_1_91_0.tar.gz  100% [=======================================================================================================================================================================================>]  232.53M   58.87MB/s
                          [Files: 1  Bytes: 232.53M [54.89MB/s] Redirects: 0  Todo: 0  Errors: 0                                                                                                                   ]
[root@10d5486a3e37 ~]# tar -xf boost_1_91_0.tar.gz
[root@10d5486a3e37 ~]# echo "#include <boost/json/src.hpp>
#include <string>
#include <iostream>

int main(int argc, char **argv)
{
    boost::json::value document;

    boost::json::object &object = document.emplace_object();
    std::string tmp = boost::json::serialize(document);
    std::cout << tmp << std::endl;
    return 0;
}" > test.cpp
[root@10d5486a3e37 ~]# g++ -std=c++23 -g3 -O3 -I/root/boost_1_91_0/  test.cpp -o test && valgrind --tool=memcheck --track-origins=yes ./test
==216== Memcheck, a memory error detector
==216== Copyright (C) 2002-2026, and GNU GPL'd, by Julian Seward et al.
==216== Using Valgrind-3.27.0 and LibVEX; rerun with -h for copyright info
==216== Command: ./test
==216== 
==216== Conditional jump or move depends on uninitialised value(s)
==216==    at 0x408A97: boost::json::value::destroy() (value.ipp:811)
==216==    by 0x402463: emplace_object (value.ipp:654)
==216==    by 0x402463: main (test.cpp:9)
==216==  Uninitialised value was created by a stack allocation
==216==    at 0x402450: main (test.cpp:6)
==216== 
==216== Conditional jump or move depends on uninitialised value(s)
==216==    at 0x408A9F: boost::json::value::destroy() (value.ipp:811)
==216==    by 0x402463: emplace_object (value.ipp:654)
==216==    by 0x402463: main (test.cpp:9)
==216==  Uninitialised value was created by a stack allocation
==216==    at 0x402450: main (test.cpp:6)
==216== 
==216== Conditional jump or move depends on uninitialised value(s)
==216==    at 0x408AA7: boost::json::value::destroy() (value.ipp:811)
==216==    by 0x402463: emplace_object (value.ipp:654)
==216==    by 0x402463: main (test.cpp:9)
==216==  Uninitialised value was created by a stack allocation
==216==    at 0x402450: main (test.cpp:6)
==216== 
{}
==216== Conditional jump or move depends on uninitialised value(s)
==216==    at 0x4081CD: boost::json::object::~object() (object.ipp:293)
==216==    by 0x402504: main (test.cpp:13)
==216==  Uninitialised value was created by a stack allocation
==216==    at 0x402450: main (test.cpp:6)
==216== 
==216== Conditional jump or move depends on uninitialised value(s)
==216==    at 0x4081DC: release (storage_ptr.hpp:107)
==216==    by 0x4081DC: ~storage_ptr (storage_ptr.hpp:141)
==216==    by 0x4081DC: boost::json::object::~object() (object.ipp:298)
==216==    by 0x402504: main (test.cpp:13)
==216==  Uninitialised value was created by a stack allocation
==216==    at 0x402450: main (test.cpp:6)
==216== 
==216== 
==216== HEAP SUMMARY:
==216==     in use at exit: 0 bytes in 0 blocks
==216==   total heap usage: 2 allocs, 2 frees, 74,752 bytes allocated
==216== 
==216== All heap blocks were freed -- no leaks are possible
==216== 
==216== For lists of detected and suppressed errors, rerun with: -s
==216== ERROR SUMMARY: 5 errors from 5 contexts (suppressed: 0 from 0)

[root@10d5486a3e37 ~]# g++ -std=c++23 -g3 -O3 -fno-lifetime-dse -I/root/boost_1_91_0/  test.cpp -o test && valgrind --tool=memcheck --track-origins=yes ./test
==223== Memcheck, a memory error detector
==223== Copyright (C) 2002-2026, and GNU GPL'd, by Julian Seward et al.
==223== Using Valgrind-3.27.0 and LibVEX; rerun with -h for copyright info
==223== Command: ./test
==223== 
{}
==223== 
==223== HEAP SUMMARY:
==223==     in use at exit: 0 bytes in 0 blocks
==223==   total heap usage: 2 allocs, 2 frees, 74,752 bytes allocated
==223== 
==223== All heap blocks were freed -- no leaks are possible
==223== 
==223== For lists of detected and suppressed errors, rerun with: -s
==223== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

The problem seems to be that GCC 16 doesn't agree with the the code in value::emplace_string/emplace_array/emplace_object. Whether that's because of a GCC regression or latent UB, I'm not really sure. A patch like this helps the test suite pass, but there might be other locations that need a similar patch to avoid the issue.

--- boost/json/impl/value.ipp  2026-05-02 00:47:16.358839082 +0000
+++ boost/json/impl/value.ipp  2026-05-02 00:48:01.056041041 +0000
@@ -637,21 +637,24 @@
 value::
 emplace_string() noexcept
 {
-    return *::new(&str_) string(destroy());
+    storage_ptr sp = destroy();
+    return *::new(&str_) string(std::move(sp));
 }
 
 array&
 value::
 emplace_array() noexcept
 {
-    return *::new(&arr_) array(destroy());
+    storage_ptr sp = destroy();
+    return *::new(&arr_) array(std::move(sp));
 }
 
 object&
 value::
 emplace_object() noexcept
 {
-    return *::new(&obj_) object(destroy());
+    storage_ptr sp = destroy();
+    return *::new(&obj_) object(std::move(sp));
 }
 
 void

GCC docs about the flag in question:

-fno-lifetime-dse

In C++ the value of an object is only affected by changes within its lifetime: when the constructor begins, the object has an indeterminate value, and any changes during the lifetime of the object are dead when the object is destroyed. Normally dead store elimination will take advantage of this; if your code relies on the value of the object storage persisting beyond the lifetime of the object, you can use this flag to disable this optimization. To preserve stores before the constructor starts (e.g. because your operator new clears the object storage) but still treat the object as dead after the destructor, you can use -flifetime-dse=1. The default behavior can be explicitly selected with -flifetime-dse=2. -flifetime-dse=0 is equivalent to -fno-lifetime-dse.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions