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.
It seems that GCC 16.1.0 breaks boost::json. Valgrind shows accesses to uninitialized memory on simple examples such as:
Example compilation:
Disassembly also confirms that the value really is uninitialized:
I have done some testing and apparently passing -fno-lifetime-dse eliminates the problem:
Disassembly confirms sp and k are properly initialized with -fno-lifetime-dse:
Full test log on Fedora 44:
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.
GCC docs about the flag in question: