Repository navigation
Segfault in rfl::json::read on GCC 12 -O2: read_struct builds its view over a T whose lifetime never began #719
Description
Summary
rfl::json::read<T>() segfaults with GCC 12 at -O2 for certain structs. The generated field view holds garbage pointers, and NamedTupleParser::handle_one_missing_field then placement-news through one of them.
Parser_default::read_struct allocates a raw byte buffer, reinterprets it as T*, and builds the view by binding to members of an object whose lifetime never began:
https://github.com/getml/reflect-cpp/blob/main/include/rfl/parsing/Parser_default.hpp#L684-L687
alignas(T) unsigned char buf[sizeof(T)]{};
auto ptr = internal::ptr_cast<T*>(&buf);
auto view = ProcessorsType::template process<T>(to_view(*ptr));Compiling that translation unit with -fno-strict-aliasing makes the crash go away, which points at TBAA acting on the unsigned char / T type pun.
This is still present on main — the three lines above are unchanged since at least 0.18.0.
Reproducer
Self-contained; needs no input file.
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <cstdint>
#include <iostream>
#include <optional>
namespace { // internal linkage is required to trigger it
struct Inner {
std::optional<std::uint32_t> a;
std::optional<int> b;
};
struct Outer {
std::optional<std::uint32_t> f1;
std::optional<std::uint32_t> f2;
std::optional<std::uint32_t> f3;
std::optional<std::uint32_t> f4;
std::optional<std::uint32_t> f5;
std::optional<std::uint32_t> f6;
std::optional<std::uint32_t> f7;
std::optional<std::uint32_t> f8;
std::optional<std::uint32_t> f9;
std::optional<std::uint32_t> f10;
std::optional<std::uint32_t> f11;
std::optional<Inner> inner; // an optional nested aggregate is required to trigger it
};
} // namespace
int main() {
const auto res = rfl::json::read<Outer>("{}");
std::cout << (res ? "OK" : "parse error") << std::endl;
}$ R=/path/to/reflect-cpp-0.25.0
$ gcc-12 -O2 -I "$R/include/rfl/thirdparty" -c "$R/src/yyjson.c" -o yyjson.o
$ g++-12 -std=c++20 -O2 -I "$R/include" \
repro.cpp "$R/src/reflectcpp.cpp" "$R/src/reflectcpp_json.cpp" yyjson.o -o repro
$ ./repro
Segmentation fault (core dumped)Expected OK — every field is optional and the input is {}.
Backtrace
Program received signal SIGSEGV, Segmentation fault.
NamedTupleParser<...>::handle_one_missing_field<0> (...)
at include/rfl/parsing/NamedTupleParser.hpp:305
305 ::new (rfl::get<_i>(_view)) ValueType();
#4 Parser<..., (anonymous namespace)::Outer, ...>::read_struct (...)
at include/rfl/parsing/Parser_default.hpp:247
#5 Parser<..., (anonymous namespace)::Outer, ...>::read (...)
at include/rfl/parsing/Parser_default.hpp:94
#6 rfl::json::read<(anonymous namespace)::Outer> (...) at include/rfl/json/read.hpp:59
#7 main () at repro.cpp:39
The view's stored field pointers are already garbage on entry — the first one is 0xfffffffffffffff8:
_view = {values_ = {static size_ = 12, static num_bytes_ = 96,
data_ = {_M_elems = "\370\377\377\377\377\377\377\377..."}}}
Field reflection itself is fine: rfl::internal::num_fields<Outer> is 12 and rfl::fields<Outer>() reports the correct names and types at every -O level. Only the parse is miscompiled.
What does and does not trigger it
Everything below is the reproducer above, varied one axis at a time, reflect-cpp 0.25.0.
| variation | result |
|---|---|
g++-12 -O2 |
SIGSEGV |
g++-12 -O0 / -O1 / -O3 |
OK |
g++-12 -O2 -fno-strict-aliasing |
OK |
g++-11 (11.5.0) -O2 |
OK |
g++-14 (14.3.0) -O2 |
OK |
| struct at namespace scope instead of an anonymous namespace | OK |
std::optional<Inner> inner replaced by a 12th std::optional<std::uint32_t> |
OK |
fewer fields (11 or below, keeping inner) |
OK |
rfl::json::read<Outer, rfl::DefaultIfMissing> (uses read_struct_with_default, a real T{}) |
OK |
So it needs all three of: internal linkage, an std::optional<NestedAggregate> member, and enough fields. The field threshold is version-dependent — on 0.18.0 the same reproducer crashes from 10 fields, on 0.25.0 from 12 — which is what you would expect from an inlining/aliasing heuristic rather than a fixed limit.
The DefaultIfMissing row is the interesting contrast: that path goes through read_struct_with_default, which builds the view over a properly constructed auto t = T{} instead of a reinterpreted byte buffer, and it does not crash.
Note on ptr_cast
include/rfl/internal/ptr_cast.hpp says:
Normally, we would use
std::launder(reinterpret_cast<...>(...)), but there are weird issues on GCC 12 under certain compiler settings, so we are using this workaround instead.
That looks like the same GCC 12 behaviour surfacing elsewhere. Dropping std::launder removes the diagnostic but leaves the underlying problem: no T object's lifetime has begun in buf, so binding the view to *ptr's members is UB regardless of how the pointer is formed.
I also tried marking the cast target __attribute__((__may_alias__)), which did not help — consistent with the corruption being in constructing the view rather than in later stores through it.
Environment
- reflect-cpp 0.25.0 (also reproduced on 0.18.0;
read_structis byte-identical onmain) - g++ 12.5.0, x86_64 Linux,
-std=c++20 - Not reproducible on g++ 11.5.0 or 14.3.0
Workaround
Building the translation unit that instantiates rfl::json::read with -fno-strict-aliasing avoids it. The flag is per-TU and survives LTO even when the link line omits it.
Activity
liuzicheng1987 commentedon Sep 7, 2026
@shaulnv thanks for pointing this out. I remember that there was a similar issue with GCC 12 (but not other compiler). I will take a look.
liuzicheng1987 commentedon Oct 18, 2026
@shaulnv I cannot reproduce the issue. I copied your code into a file called "test_gcc12.cpp" and then ran the following:
And it worked just fine.
liuzicheng1987 commentedon Oct 18, 2026
@shaulnv , there is an open PR which should resolve the issue, but it leads to some performance regression as well as other problems, so I am unsure how to proceed. But, can you confirm that this resolves the issue?
#737
liuzicheng1987 commentedon Oct 18, 2026
@shaulnv alternatively, there is this less invasive approach (not very polished):
https://github.com/getml/reflect-cpp/tree/f/resolve_g%2B%2B12_issue
Could you try this as well?
Altren commentedon Oct 18, 2026
I managed to reproduce issue inside compiler explorer. Replacing
using DataType = std::array<unsigned char, num_bytes_>;withusing DataType = unsigned char[num_bytes_];and removing no longer needed.data()somehow solves the issue. Could it be a compiler bug?Both of your fixes also help. And add some new issues, but tests already reveal that