Skip to content

Fix ChArchiveOut serializing a dangling temporary for std::map and st… - #774

Open
DanNegrut wants to merge 1 commit into
mainfrom
fix/archive-map-dangling-temporary
Open

Fix ChArchiveOut serializing a dangling temporary for std::map and st…#774
DanNegrut wants to merge 1 commit into
mainfrom
fix/archive-map-dangling-temporary

Conversation

@DanNegrut

@DanNegrut DanNegrut commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #776

…d::unordered_map

Dereferencing a std::map<T, Tv> iterator yields std::pair<const T, Tv>&, but the out() wrappers for std::map and std::unordered_map declared their ChNameValue with a non-const key. The two types differ, so the compiler materialized a temporary pair, and ChNameValue stores only a raw pointer to its value (it takes const T& and keeps (T*)&mvalue). Two things followed: the archive wrote a copy of the element rather than the element itself, and that copy had already been destroyed by the time it was written, so the write read freed memory.

The visible effect depended on the value type. A trivially destructible type such as ChVector3d usually still read back its old bytes and looked correct, which is why this survived so long. A polymorphic type with a non-trivial destructor lost everything below its base class: serializing a std::map<std::string, ChLinkTSDA> produced a 254 byte record holding only the ChObj fields, with no _version_ChLinkTSDA, no spring coefficients and no bodies. Reported in #507, where it originally presented as a segmentation fault.

The fix declares the key const in the two out() wrappers, so the ChNameValue binds directly to the real map element and no temporary is created. The in() path is deliberately left alone: _wrap_pair::ArchiveIn writes into first, so a const key there would assign through a pointer to a const object. Because member functions of a class template are instantiated only when used, ArchiveIn is never instantiated on the const-keyed specialization, and only ArchiveOut, which merely reads first, is affected.

The naive alternative, giving the temporary a name so that it outlives the call, would compile and stop the crash but would keep serializing a copy. That is not equivalent: ChLink's copy constructor nulls both body pointers, so it would trade a loud bug for a silent one.

Adds five regression tests to utest_CH_archive. They assert that the archive does not copy the container element, which is deterministic, rather than asserting on how the resulting undefined behavior happens to manifest, which varies with type and optimizer. vector and list are included as controls. With the two-line change reverted, the map and unordered_map tests fail (one copy observed, and the value reads back as the test payload's destructor poison) while the vector and list controls still pass. This file previously had no STL container coverage at all, and the only map in demo_CH_archive is an unordered_map<int, double> of PODs with no assertions, which is why nothing in tree caught this.

Unrelated and pre-existing, noted here only because this fix makes it visible in serialized output: ChObj's copy constructor never assigns m_tag, so any copied or cloned Chrono object carries a garbage tag. Confirmed without any serialization involved, where a ChBody tagged 77 reports 32765 after a copy. That is left for a separate report.

Verified with clang (ROCm 7.1) on Windows, Release: all 10 core unit test binaries pass (44 cases) and demo_CH_archive still round-trips.

…d::unordered_map

Dereferencing a std::map<T, Tv> iterator yields std::pair<const T, Tv>&, but the out()
wrappers for std::map and std::unordered_map declared their ChNameValue with a non-const
key. The two types differ, so the compiler materialized a temporary pair, and ChNameValue
stores only a raw pointer to its value (it takes const T& and keeps (T*)&mvalue). Two
things followed: the archive wrote a copy of the element rather than the element itself,
and that copy had already been destroyed by the time it was written, so the write read
freed memory.

The visible effect depended on the value type. A trivially destructible type such as
ChVector3d usually still read back its old bytes and looked correct, which is why this
survived so long. A polymorphic type with a non-trivial destructor lost everything below
its base class: serializing a std::map<std::string, ChLinkTSDA> produced a 254 byte record
holding only the ChObj fields, with no _version_ChLinkTSDA, no spring coefficients and no
bodies. Reported in #507, where it originally presented as a segmentation fault.

The fix declares the key const in the two out() wrappers, so the ChNameValue binds directly
to the real map element and no temporary is created. The in() path is deliberately left
alone: _wrap_pair::ArchiveIn writes into first, so a const key there would assign through a
pointer to a const object. Because member functions of a class template are instantiated
only when used, ArchiveIn is never instantiated on the const-keyed specialization, and only
ArchiveOut, which merely reads first, is affected.

The naive alternative, giving the temporary a name so that it outlives the call, would
compile and stop the crash but would keep serializing a copy. That is not equivalent:
ChLink's copy constructor nulls both body pointers, so it would trade a loud bug for a
silent one.

Adds five regression tests to utest_CH_archive. They assert that the archive does not copy
the container element, which is deterministic, rather than asserting on how the resulting
undefined behavior happens to manifest, which varies with type and optimizer. vector and
list are included as controls. With the two-line change reverted, the map and unordered_map
tests fail (one copy observed, and the value reads back as the test payload's destructor
poison) while the vector and list controls still pass. This file previously had no STL
container coverage at all, and the only map in demo_CH_archive is an
unordered_map<int, double> of PODs with no assertions, which is why nothing in tree caught
this.

Unrelated and pre-existing, noted here only because this fix makes it visible in serialized
output: ChObj's copy constructor never assigns m_tag, so any copied or cloned Chrono object
carries a garbage tag. Confirmed without any serialization involved, where a ChBody tagged
77 reports 32765 after a copy. That is left for a separate report.

Verified with clang (ROCm 7.1) on Windows, Release: all 10 core unit test binaries pass
(44 cases) and demo_CH_archive still round-trips.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DanNegrut
DanNegrut requested a review from rserban July 27, 2026 15:32
@rserban
rserban requested a review from dariomangoni July 27, 2026 15:43
@rserban

rserban commented Jul 27, 2026

Copy link
Copy Markdown
Member

@DanNegrut - I assigned Dario Mangoni as a reviewer. The archiving stuff was implemented at Parma and I am not very familiar with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ChArchiveOut serializes a dangling temporary for std::map and std::unordered_map

2 participants